~kivy-team/kivy/master

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
'''
Interactive launcher
====================

.. versionadded:: 1.3.0

.. versionchanged:: 1.9.2
    The interactive launcher has been deprecated.

The :class:`InteractiveLauncher` provides a user-friendly python shell
interface to an :class:`App` so that it can be prototyped and debugged
interactively.

.. note::

    The Kivy API intends for some functions to only be run once or before the
    main EventLoop has started. Methods that can normally be called during the
    course of an application will work as intended, but specifically overriding
    methods such as :meth:`on_touch` dynamically leads to trouble.

Creating an InteractiveLauncher
-------------------------------

Take your existing subclass of :class:`App` (this can be production code) and
pass an instance to the :class:`InteractiveLauncher` constructor. ::

    from kivy.interactive import InteractiveLauncher
    from kivy.app import App
    from kivy.uix.button import Button

    class MyApp(App):
        def build(self):
            return Button(text='Hello Shell')

    launcher = InteractiveLauncher(MyApp())
    launcher.run()

After pressing *enter*, the script will return. This allows the interpreter to
continue running. Inspection or modification of the :class:`App` can be done
safely through the InteractiveLauncher instance or the provided
:class:`SafeMembrane` class instances.

.. note::

    If you want to test this example, start Python without any file to have
    already an interpreter, and copy/paste all the lines. You'll still have the
    interpreter at the end + the kivy application running.

Interactive Development
-----------------------

IPython provides a fast way to learn the Kivy API. The :class:`App` instance
and all of it's attributes, including methods and the entire widget tree,
can be quickly listed by using the '.' operator and pressing 'tab'. Try this
code in an Ipython shell. ::

    from kivy.interactive import InteractiveLauncher
    from kivy.app import App
    from kivy.uix.widget import Widget
    from kivy.graphics import Color, Ellipse

    class MyPaintWidget(Widget):
        def on_touch_down(self, touch):
            with self.canvas:
                Color(1, 1, 0)
                d = 30.
                Ellipse(pos=(touch.x - d/2, touch.y - d/2), size=(d, d))


    class TestApp(App):
        def build(self):
            return Widget()


    i = InteractiveLauncher(TestApp())
    i.run()
    i.       # press 'tab' to list attributes of the app
    i.root.  # press 'tab' to list attributes of the root widget

    # App is boring. Attach a new widget!
    i.root.add_widget(MyPaintWidget())

    i.safeIn()
    # The application is now blocked.
    # Click on the screen several times.
    i.safeOut()
    # The clicks will show up now

    # Erase artwork and start over
    i.root.canvas.clear()

.. note::

    All of the proxies used in the module store their referent in the
    :attr:`_ref` attribute, which can be accessed directly if needed, such as
    for getting doc strings. :func:`help` and :func:`type` will access the
    proxy, not its referent.

Directly Pausing the Application
--------------------------------

Both the :class:`InteractiveLauncher` and :class:`SafeMembrane` hold internal
references to the :class:`EventLoop`'s 'safe' and 'confirmed'
:class:`threading.Event` objects. You can use their safing methods to control
the application manually.

:meth:`SafeMembrane.safeIn` will cause the application to pause and
:meth:`SafeMembrane.safeOut` will allow a paused application
to continue running. This is potentially useful for scripting actions into
functions that need the screen to update etc.

.. note::

    The pausing is implemented via the
    :class:`Clocks' <kivy.clock.Clock>`
    :meth:`~kivy.clock.ClockBase.schedule_once` method
    and occurs before the start of each frame.

Adding Attributes Dynamically
-----------------------------

.. note::

    This module uses threading and object proxies to encapsulate the running
    :class:`App`. Deadlocks and memory corruption can occur if making direct
    references inside the thread without going through the provided proxy(s).

The :class:`InteractiveLauncher` can have attributes added to it exactly like a
normal object and if these were created from outside the membrane, they will
not be threadsafe because the external references to them in the python
interpreter do not go through InteractiveLauncher's membrane behavior,
inherited from :class:`SafeMembrane`.

To threadsafe these external references, simply assign them to
:class:`SafeMembrane` instances of themselves like so::

    from kivy.interactive import SafeMembrane

    interactiveLauncher.attribute = myNewObject
    # myNewObject is unsafe
    myNewObject = SafeMembrane(myNewObject)
    # myNewObject is now safe. Call at will.
    myNewObject.method()

TODO
====

Unit tests, examples, and a better explanation of which methods are safe in a
running application would be nice. All three would be excellent.

Could be re-written with a context-manager style i.e. ::

    with safe:
        foo()

Any use cases besides compacting code?

'''

__all__ = ('SafeMembrane', 'InteractiveLauncher')

import inspect
from threading import Thread, Event

from kivy.app import App
from kivy.base import EventLoop
from kivy.clock import Clock
from kivy.utils import deprecated


def safeWait(dt):
    EventLoop.confirmed.set()
    EventLoop.safe.wait()
    EventLoop.confirmed.clear()


def unwrap(ob):
    while type(ob) == SafeMembrane:
        ob = ob._ref
    return ob


class SafeMembrane(object):
    '''
    This help is for a proxy object. Did you want help on the proxy's referent
    instead? Try using help(<instance>._ref)

    The SafeMembrane is a threadsafe proxy that also returns attributes as new
    thread-safe objects
    and makes thread-safe method calls, preventing thread-unsafe objects
    from leaking into the user's environment.
    '''

    __slots__ = ('_ref', 'safe', 'confirmed')

    def __init__(self, ob, *args, **kwargs):
        self.confirmed = EventLoop.confirmed
        self.safe = EventLoop.safe
        self._ref = ob

    def safeIn(self):
        """Provides a thread-safe entry point for interactive launching."""
        self.safe.clear()
        Clock.schedule_once(safeWait, -1)
        self.confirmed.wait()

    def safeOut(self):
        """Provides a thread-safe exit point for interactive launching."""
        self.safe.set()

    def isMethod(self, fn):
        return inspect.ismethod(fn)

    # Everything from this point on is just a series of thread-safing proxy
    # methods that make calls against _ref and threadsafe whenever data will be
    # written to or if a method will be called. SafeMembrane instances should
    # be unwrapped whenever passing them into the thread
    # use type() to determine if an object is a SafeMembrane while debugging
    def __repr__(self):
        return self._ref.__repr__()

    def __call__(self, *args, **kw):
        self.safeIn()
        args = list(map(unwrap, args))
        for k in list(kw.keys()):
            kw[k] = unwrap(kw[k])
        r = self._ref(*args, **kw)
        self.safeOut()
        if r is not None:
            return SafeMembrane(r)

    def __getattribute__(self, attr, oga=object.__getattribute__):
        if attr.startswith('__') or attr == '_ref':
            subject = oga(self, '_ref')
            if attr == '_ref':
                return subject
            return getattr(subject, attr)
        return oga(self, attr)

    def __getattr__(self, attr, oga=object.__getattribute__):
        r = getattr(oga(self, '_ref'), attr)
        return SafeMembrane(r)

    def __setattr__(self, attr, val, osa=object.__setattr__):
        if (attr == '_ref'
                or hasattr(type(self), attr) and not attr.startswith('__')):
            osa(self, attr, val)
        else:
            self.safeIn()
            val = unwrap(val)
            setattr(self._ref, attr, val)
            self.safeOut()

    def __delattr__(self, attr, oda=object.__delattr__):
        self.safeIn()
        delattr(self._ref, attr)
        self.safeOut()

    def __bool__(self):
        return bool(self._ref)

    def __getitem__(self, arg):
        return SafeMembrane(self._ref[arg])

    def __setitem__(self, arg, val):
        self.safeIn()
        val = unwrap(val)
        self._ref[arg] = val
        self.safeOut()

    def __delitem__(self, arg):
        self.safeIn()
        del self._ref[arg]
        self.safeOut()

    def __getslice__(self, i, j):
        return SafeMembrane(self._ref[i:j])

    def __setslice__(self, i, j, val):
        self.safeIn()
        val = unwrap(val)
        self._ref[i:j] = val
        self.safeOut()

    def __delslice__(self, i, j):
        self.safeIn()
        del self._ref[i:j]
        self.safeOut()

    def __enter__(self, *args, **kwargs):
        self.safeIn()
        self._ref.__enter__(*args, **kwargs)

    def __exit__(self, *args, **kwargs):
        self._ref.__exit__(*args, **kwargs)
        self.safeOut()


class InteractiveLauncher(SafeMembrane):
    '''
    Proxy to an application instance that launches it in a thread and
    then returns and acts as a proxy to the application in the thread.
    '''

    __slots__ = ('_ref', 'safe', 'confirmed', 'thread', 'app')

    @deprecated
    def __init__(self, app=None, *args, **kwargs):
        if app is None:
            app = App()
        EventLoop.safe = Event()
        self.safe = EventLoop.safe
        self.safe.set()
        EventLoop.confirmed = Event()
        self.confirmed = EventLoop.confirmed
        self.app = app

        def startApp(app=app, *args, **kwargs):
            app.run(*args, **kwargs)

        self.thread = Thread(target=startApp, *args, **kwargs)

    def run(self):
        self.thread.start()
        # Proxy behavior starts after this is set. Before this point, attaching
        # widgets etc can only be done through the Launcher's app attribute
        self._ref = self.app

    def stop(self):
        EventLoop.quit = True
        self.thread.join()

    # Act like the app instance even before _ref is set
    def __repr__(self):
        return self.app.__repr__()