Stav 23.06.2026
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
'''
|
||||
Interactive launcher
|
||||
====================
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
.. deprecated:: 1.10.0
|
||||
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 its 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 isinstance(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__()
|
||||
Reference in New Issue
Block a user