Skip to content

Commit

Permalink
added asynchronous event loops and pylab support; fixes #21
Browse files Browse the repository at this point in the history
  • Loading branch information
stevengj committed Jul 18, 2013
1 parent abe8b98 commit 23e4158
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 10 deletions.
57 changes: 54 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ Keyword arguments can also be passed. For example, matplotlib's
[pylab](http://matplotlib.org/) uses keyword arguments to specify plot
options, and this functionality is accessed from Julia by:

@pyimport pylab
@pylab as plt
x = linspace(0,2*pi,1000); y = sin(3*x + 4*cos(2*x));
pylab.plot(x, y; color="red", linewidth=2.0, linestyle="--")
pylab.show()
plt.plot(x, y; color="red", linewidth=2.0, linestyle="--")

The `@pylab` command is a specialized macro that is like `@pyimport
pylab` except that it also starts a GUI event loop (see below) so that
the pylab plots appear interactively and without blocking.

Arbitrary Julia functions can be passed to Python routines taking
function arguments. For example, to find the root of cos(x) - x,
Expand Down Expand Up @@ -241,6 +244,54 @@ accomplished using:
* The Python version number is stored in the global variable
`pyversion::VersionNumber`.

### GUI Event Loops

For Python packages that have a graphical user interface (GUI),
notably plotting packages like pylab (or MayaVi or Chaco), it is
convenient to start the GUI event loop (which processes things like
mouse clicks) as an asynchronous task within Julia, so that the GUI is
responsive without blocking Julia's input prompt. PyCall includes
functions to implement these event loops for some of the most common
cross-platform [GUI
toolkits](http://en.wikipedia.org/wiki/Widget_toolkit):
[wxWidgets](http://www.wxwidgets.org/), [GTK+](http://www.gtk.org/),
and [Qt](http://qt-project.org/) (via the [PyQt4](http://wiki.python.org/moin/PyQt4) or [PySide](http://qt-project.org/wiki/PySide)
Python modules).

You can set a GUI event loop via:

* `pygui_start(gui::Symbol=pygui())`. Here, `gui` is either `:wx`,
`:gtk`, or `:qt` to start the respective toolkit's event loop. It
defaults to the return value of `pygui()`, which returns a current
default GUI (see below). Passing a `gui` argument also changes the
default GUI, equivalent to calling `pygui(gui)` below. You may
start event loops for more than one GUI toolkit (to run simultaneously).
Calling `pygui_start` more than once for a given toolkit does nothing
(except to change the current `pygui` default).

* `pygui()`: return the current default GUI toolkit (`Symbol`). If
the default GUI has not been set already, this is the first of
`:wx`, `:gtk`, or `:qt` for which the corresponding Python package
is installed. `pygui(gui::Symbol)` changes the default GUI to
`gui`.

* `pygui_stop(gui::Symbol=pygui())`: Stop any running event loop for `gui`
(which defaults to the current return value of `pygui`). Returns
`true` if an event loop was running, and `false` otherwise.

To use these GUI facilities with some Python libraries, it is enough
to simply start the appropriate toolkit's event-loop before importing
the library. However, in other cases it is necessary to explicitly
tell the library which GUI toolkit to use and that an interactive mode
is desired. To make this easier, PyCall may provide a specialized
importing macro for key Python libraries. Currently, only
matplotlib's pylab plotting library is supported in this way:

* `@pylab [as NAME]` imports the `pylab` plotting library, starting it
in interactive mode with the default GUI toolkit (and starting the
event loop if needed). This is otherwise similar to `@pyimport pylab
[as NAME]`.

### Low-level Python API access

If you want to call low-level functions in the Python C API, you can
Expand Down
23 changes: 16 additions & 7 deletions src/PyCall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export pyinitialize, pyfinalize, pycall, pyimport, pybuiltin, PyObject,
pysym, PyPtr, pyincref, pydecref, pyversion, PyArray, PyArray_Info,
pyerr_check, pyerr_clear, pytype_query, PyAny, @pyimport, PyWrapper,
PyDict, pyisinstance, pywrap, pytypeof, pyeval, pyhassym,
PyVector, pystring, pyraise, pytype_mapping
PyVector, pystring, pyraise, pytype_mapping, pygui, pygui_start,
pygui_stop, pygui_stop_all, @pylab

import Base.size, Base.ndims, Base.similar, Base.copy, Base.getindex,
Base.setindex!, Base.stride, Base.convert, Base.pointer,
Expand Down Expand Up @@ -356,6 +357,7 @@ function pyfinalize()
global mpf
global mpc
if initialized::Bool
pygui_stop_all()
pydecref(mpc::PyObject)
pydecref(mpf::PyObject)
pydecref(mpmath::PyObject)
Expand Down Expand Up @@ -386,6 +388,7 @@ end
#########################################################################

include("exception.jl")
include("gui.jl")

#########################################################################

Expand Down Expand Up @@ -550,14 +553,20 @@ function modulename(e::Expr)
end
end

# separate this function in order to make it easier to write more
# pyimport-like functions
pyimport_name(name, optional_varname) =
let len = length(optional_varname)
len > 0 && (len != 2 || optional_varname[1] != :as) ?
throw(ArgumentError("usage @pyimport module [as name]")) :
(len == 2 ? optional_varname[2] :
typeof(name) == Symbol ? name :
throw(ArgumentError("$mname is not a valid module variable name, use @pyimport $mname as <name>")))
end

macro pyimport(name, optional_varname...)
mname = modulename(name)
len = length(optional_varname)
Name = len > 0 && (len != 2 || optional_varname[1] != :as) ?
throw(ArgumentError("usage @pyimport module [as name]")) :
(len == 2 ? optional_varname[2] :
typeof(name) == Symbol ? name :
throw(ArgumentError("$mname is not a valid module variable name, use @pyimport $mname as <name>")))
Name = pyimport_name(name, optional_varname)
quote
$(esc(Name)) = pywrap(pyimport($mname))
nothing
Expand Down
182 changes: 182 additions & 0 deletions src/gui.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# GUI event loops and toolkit integration for Python, most importantly
# to support plotting in a window without blocking. Currently, we
# support wxWidgets, Qt4, and GTK+.

############################################################################

# global variable to specify default GUI toolkit to use
gui = :wx # one of :wx, :qt, or :gtk

pyexists(mod) = try
pyimport(mod)
true
catch
false
end

pygui_works(gui::Symbol) =
((gui == :wx && pyexists("wx")) ||
(gui == :gtk && pyexists("gtk")) ||
(gui == :qt && (pyexists("PyQt4") || pyexists("PySide"))))

# get or set the default GUI; doesn't affect running GUI
function pygui()
global gui
if pygui_works(gui::Symbol)
return gui::Symbol
else
for g in (:wx, :gtk, :qt)
if pygui_works(g)
gui::Symbol = g
return gui::Symbol
end
end
error("No supported Python GUI toolkit is installed.")
end
end
function pygui(g::Symbol)
global gui
if g != gui::Symbol
if g != :wx && g != :gtk && g != :qt
throw(ArgumentError("invalid gui $g"))
elseif !pygui_works(g)
error("Python GUI toolkit for $g is not installed.")
end
gui::Symbol = g
end
return g
end

############################################################################
# Event loops for various toolkits.

# call doevent(status) every sec seconds
function install_doevent(doevent::Function, sec::Real)
timeout = Base.TimeoutAsyncWork(doevent)
Base.start_timer(timeout,sec,sec)
return timeout
end

# GTK:
function gtk_eventloop(sec::Real=50e-3)
gtk = pyimport("gtk")
events_pending = gtk["events_pending"]
main_iteration = gtk["main_iteration"]
function doevent(async, status::Int32) # handle all pending
while pycall(events_pending, Bool)
pycall(main_iteration, PyObject)
end
end
install_doevent(doevent, sec)
end

# Qt4: (PyQt4 or PySide module)
function qt_eventloop(QtModule="PyQt4", sec::Real=50e-3)
QtCore = pyimport("$QtModule.QtCore")
instance = QtCore["QCoreApplication"]["instance"]
AllEvents = QtCore["QEventLoop"]["AllEvents"]
processEvents = QtCore["QCoreApplication"]["processEvents"]
maxtime = PyObject(50)
function doevent(async, status::Int32)
app = pycall(instance, PyObject)
if app.o != (pynothing::PyObject).o
app["_in_event_loop"] = true
pycall(processEvents, PyObject, AllEvents, maxtime)
end
end
install_doevent(doevent, sec)
end

# wx: (based on IPython/lib/inputhookwx.py, which is 3-clause BSD-licensed)
function wx_eventloop(sec::Real=50e-3)
wx = pyimport("wx")
GetApp = wx["GetApp"]
EventLoop = wx["EventLoop"]
EventLoopActivator = wx["EventLoopActivator"]
function doevent(async, status::Int32)
app = pycall(GetApp, PyObject)
if app.o != (pynothing::PyObject).o
app["_in_event_loop"] = true
evtloop = pycall(EventLoop, PyObject)
ea = pycall(EventLoopActivator, PyObject, evtloop)
Pending = evtloop["Pending"]
Dispatch = evtloop["Dispatch"]
while pycall(Pending, Bool)
pycall(Dispatch, PyObject)
end
pydecref(ea) # deactivate event loop
pycall(app["ProcessIdle"], PyObject)
end
end
install_doevent(doevent, sec)
end

# cache running event loops (so that we don't start any more than once)
const eventloops = (Symbol=>TimeoutAsyncWork)[]

function pygui_start(gui::Symbol=pygui(), sec::Real=50e-3)
pygui(gui)
if !haskey(eventloops, gui)
if gui == :wx
eventloops[gui] = wx_eventloop(sec)
elseif gui == :gtk
eventloops[gui] = gtk_eventloop(sec)
elseif gui == :qt
try
eventloops[gui] = qt_eventloop("PyQt4", sec)
catch
eventloops[gui] = qt_eventloop("PySide", sec)
end
else
throw(ArgumentError("unsupported GUI type $gui"))
end
end
gui
end

function pygui_stop(gui::Symbol=pygui())
if haskey(eventloops, gui)
Base.stop_timer(delete!(eventloops, gui))
true
else
false
end
end

pygui_stop_all() = for gui in keys(eventloops); pygui_stop(gui); end

############################################################################
# Special support for matplotlib and pylab, to make them a bit easier to use,
# since you need to jump through some hoops to tell matplotlib which GUI to
# use and to employ interactive mode.

# map gui to corresponding matplotlib backend
const gui2matplotlib = [ :wx => "WXAgg", :gtk => "GTKAgg", :qt => "Qt4Agg" ]

function pymatplotlib(gui::Symbol=pygui())
pygui_start(gui)
m = pyimport("matplotlib")
m[:use](gui2matplotlib[gui])
m[:interactive](true)
return m
end

# We monkey-patch pylab.show to ensure that it is non-blocking, as
# matplotlib does not reliably detect that our event-loop is running.
# (Note that some versions of show accept a "block" keyword or directly
# as a boolean argument, so we must accept the same arguments.)
function show_noop(b=false; block=false)
nothing # no-op
end

macro pylab(optional_varname...)
Name = pyimport_name(:pylab, optional_varname)
quote
pymatplotlib()
$(esc(Name)) = pywrap(pyimport("pylab"))
$(esc(Name)).show = show_noop
nothing
end
end

############################################################################

0 comments on commit 23e4158

Please sign in to comment.