Catch MainLoop exceptions and displaying them in MessageDialogs - python

I have a wxPython application that relies on an external config file. I want provide friendly message dialogs that show up if there are any config errors. I've tried to make this work by wrapping my app.MainLoop() call in a try/except statement.
The code below works for the init code in my MainWindow frame class, but doesn't catch any exceptions that occur within the MainLoop. How can I catch these exceptions as well?
if __name__ == '__main__':
app = MyApp(0)
try:
MainWindow(None, -1, 'My Cool App')
app.MainLoop()
except ConfigParser.Error, error_message:
messagebox = wx.MessageDialog(None, error_message, 'Configuration Error', wx.OK | wx.ICON_ERROR)
messagebox.ShowModal()
I've read some mention of an OnExceptionInMainLoop method that can be overridden in the wx.App class, but the source I read must be out of date (2004) since wx.App no longer seems to have a method by that name.
EDIT:
I need to be able to catch unhandled exceptions during my mainloop so that I can further handle them and display them in error dialogs, not pass silently, and not terminate the app.
The sys.excepthook solution is too low level and doesn't play nice with the wxPython mainloop thread. While the link to the other answer does the same try/except wrapping around the mainloop which doesn't work due, once again, to wxPython spawning a different thread for the app/ui.

Don't know if this will work for a wxPython application, but in the sys module you can overwrite the excepthook attribute, which is a function called with 3 arguments, (type, value, traceback), when an uncaugth exception is caught. You can install your own function in there that handles only the exceptions you want, and call the original function for all the others.
Consult: http://docs.python.org/library/sys.html#sys.excepthook

I coded something like this for Chandler, where any unhandled exceptions pop up a window with the stack and other info, and users can put in additional comments (what did they do when it happened etc.) and submit it for Chandler developers. A bit like the Mozilla Talkback (nowadays they use Breakpad I believe) feature if you will.
To do this in wxPython, you need to provide redirect parameter to wx.App. This will pop up wx.PyOnDemandOutputWindow (you will probably want to override it to provide a nicer looking implementation).
The relevant source files in Chandler are here:
Chandler.py starts the application and sets the redirect attribute, as well as tries to catch and display error dialogs in case normal application startup fails
Application.py customizes the application object, including setting up our customized wx.PyOnDemandOutputWindow
feedback.py has the implementation for the customized wx.PyOnDemandOutputWindow; it will additionally need feedback.xrc and feedback_xrc.py

Perhaps this question might be of some use, it tries to capture all exceptions.

Posting the solution that worked for me with a very similar problem.
import wx
import sys
import traceback
class Frame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None)
panel = wx.Panel(self)
m_close = wx.Button(panel, -1, "Error")
m_close.Bind(wx.EVT_BUTTON, self.OnErr)
def OnErr(self, event):
1/0
def handleGUIException(exc_type, exc_value, exc_traceback):
err_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
err_msg += '\n Your App will now terminate'
# Here collecting traceback and some log files to be sent for debugging.
# But also possible to handle the error and continue working.
dlg = wx.MessageDialog(None, err_msg, 'Termination dialog', wx.OK | wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
sys.exit()
sys.excepthook = handleGUIException
if __name__ == '__main__':
app = wx.App(redirect=False)
top = Frame()
top.Show()
app.MainLoop()

Using sys.excepthook is very fine to me.
I find the following article of great help: wxPython: Catching Exceptions from Anywhere.

Related

Exception handled surprisingly in Pyside slots

Problem: When exceptions are raised in slots, invoked by signals, they do not seem to propagate as usual through Pythons call stack. In the example code below invoking:
on_raise_without_signal(): Will handle the exception as expected.
on_raise_with_signal(): Will print the exception and then unexpectedly print the success message from the else block.
Question: What is the reason behind the exception being handled surprisingly when raised in a slot? Is it some implementation detail/limitation of the PySide Qt wrapping of signals/slots? Is there something to read about in the docs?
PS: I initially came across that topic when I got surprising results upon using try/except/else/finally when implementing a QAbstractTableModels virtual methods insertRows() and removeRows().
# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division
import logging
import sys
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class ExceptionTestWidget(QtGui.QWidget):
raise_exception = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(ExceptionTestWidget, self).__init__(*args, **kwargs)
self.raise_exception.connect(self.slot_raise_exception)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
# button to invoke handler that handles raised exception as expected
btn_raise_without_signal = QtGui.QPushButton("Raise without signal")
btn_raise_without_signal.clicked.connect(self.on_raise_without_signal)
layout.addWidget(btn_raise_without_signal)
# button to invoke handler that handles raised exception via signal unexpectedly
btn_raise_with_signal = QtGui.QPushButton("Raise with signal")
btn_raise_with_signal.clicked.connect(self.on_raise_with_signal)
layout.addWidget(btn_raise_with_signal)
def slot_raise_exception(self):
raise ValueError("ValueError on purpose")
def on_raise_without_signal(self):
"""Call function that raises exception directly."""
try:
self.slot_raise_exception()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_without_signal() executed successfully")
def on_raise_with_signal(self):
"""Call slot that raises exception via signal."""
try:
self.raise_exception.emit()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_with_signal() executed successfully")
if (__name__ == "__main__"):
application = QtGui.QApplication(sys.argv)
widget = ExceptionTestWidget()
widget.show()
sys.exit(application.exec_())
As you've already noted in your question, the real issue here is the treatment of unhandled exceptions raised in python code executed from C++. So this is not only about signals: it also affects reimplemented virtual methods as well.
In PySide, PyQt4, and all PyQt5 versions up to 5.5, the default behaviour is to automatically catch the error on the C++ side and dump a traceback to stderr. Normally, a python script would also automatically terminate after this. But that is not what happens here. Instead, the PySide/PyQt script just carries on regardless, and many people quite rightly regard this as a bug (or at least a misfeature). In PyQt-5.5, this behaviour has now been changed so that qFatal() is also called on the C++ side, and the program will abort like a normal python script would. (I don't know what the current situation is with PySide2, though).
So - what should be done about all this? The best solution for all versions of PySide and PyQt is to install an exception hook - because it will always take precedence over the default behaviour (whatever that may be). Any unhandled exception raised by a signal, virtual method or other python code will firstly invoke sys.excepthook, allowing you to fully customise the behaviour in whatever way you like.
In your example script, this could simply mean adding something like this:
def excepthook(cls, exception, traceback):
print('calling excepthook...')
logger.error("{}".format(exception))
sys.excepthook = excepthook
and now the exception raised by on_raise_with_signal can be handled in the same way as all other unhandled exceptions.
Of course, this does imply that best practice for most PySide/PyQt applications is to use largely centralised exception handling. This often includes showing some kind of crash-dialog where the user can report unexpected errors.
According to the Qt5 docs you need to handle exceptions within the slot being invoked.
Throwing an exception from a slot invoked by Qt's signal-slot connection mechanism is considered undefined behaviour, unless it is handled within the slot
State state;
StateListener stateListener;
// OK; the exception is handled before it leaves the slot.
QObject::connect(&state, SIGNAL(stateChanged()), &stateListener, SLOT(throwHandledException()));
// Undefined behaviour; upon invocation of the slot, the exception will be propagated to the
// point of emission, unwinding the stack of the Qt code (which is not guaranteed to be exception safe).
QObject::connect(&state, SIGNAL(stateChanged()), &stateListener, SLOT(throwUnhandledException()));
If the slot was invoked directly, like a regular function call,
exceptions may be used. This is because the connection mechanism is
bypassed when invoking slots directly
In the first case you call slot_raise_exception() directly, so this is fine.
In the second case you are invoking it via the raise_exception signal, so the exception will only propagate up to the point where slot_raise_exception() is called. You need to place the try/except/else inside slot_raise_exception() for the exception to be handled correctly.
Thanks for answering guys. I found ekhumoros answer particularly useful to understand where the exceptions are handled and because of the idea to utilize sys.excepthook.
I mocked up a quick solution via context manager to temporarily extend the current sys.excepthook to record any exception in the realm of "C++ calling Python" (as it seems to happen when slots are invoked by signals or virtual methods) and possibly re-raise upon exiting the context to achieve expected control flow in try/except/else/finally blocks.
The context manager allows on_raise_with_signal to maintain the same control flow as on_raise_without_signal with the surrounding try/except/else/finally block.
# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division
import logging
import sys
from functools import wraps
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class ExceptionHook(object):
def extend_exception_hook(self, exception_hook):
"""Decorate sys.excepthook to store a record on the context manager
instance that might be used upon leaving the context.
"""
#wraps(exception_hook)
def wrapped_exception_hook(exc_type, exc_val, exc_tb):
self.exc_val = exc_val
return exception_hook(exc_type, exc_val, exc_tb)
return wrapped_exception_hook
def __enter__(self):
"""Temporary extend current exception hook."""
self.current_exception_hook = sys.excepthook
sys.excepthook = self.extend_exception_hook(sys.excepthook)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Reset current exception hook and re-raise in Python call stack after
we have left the realm of `C++ calling Python`.
"""
sys.excepthook = self.current_exception_hook
try:
exception_type = type(self.exc_val)
except AttributeError:
pass
else:
msg = "{}".format(self.exc_val)
raise exception_type(msg)
class ExceptionTestWidget(QtGui.QWidget):
raise_exception = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(ExceptionTestWidget, self).__init__(*args, **kwargs)
self.raise_exception.connect(self.slot_raise_exception)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
# button to invoke handler that handles raised exception as expected
btn_raise_without_signal = QtGui.QPushButton("Raise without signal")
btn_raise_without_signal.clicked.connect(self.on_raise_without_signal)
layout.addWidget(btn_raise_without_signal)
# button to invoke handler that handles raised exception via signal unexpectedly
btn_raise_with_signal = QtGui.QPushButton("Raise with signal")
btn_raise_with_signal.clicked.connect(self.on_raise_with_signal)
layout.addWidget(btn_raise_with_signal)
def slot_raise_exception(self):
raise ValueError("ValueError on purpose")
def on_raise_without_signal(self):
"""Call function that raises exception directly."""
try:
self.slot_raise_exception()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_without_signal() executed successfully")
def on_raise_with_signal(self):
"""Call slot that raises exception via signal."""
try:
with ExceptionHook() as exception_hook:
self.raise_exception.emit()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_with_signal() executed successfully")
if (__name__ == "__main__"):
application = QtGui.QApplication(sys.argv)
widget = ExceptionTestWidget()
widget.show()
sys.exit(application.exec_())
This way of handling expcetions is not a surprise taking into consideration that the Signal/Slot architecture proposes a loosely coupled intercation between the signals and the slots. This means that the signal should not be expecting anything to happen inside the slots.
Although timmwagener's solution was pretty clever, it should be used with caution. Probably the problem is not with how Exceptions are handled between Qt Connections, but that the signal/slot architecture is not ideal for your application. Also, that solution would not work if a slot from a different thread is connected, or a Qt.QueuedConnection is used.
A good way of tackling the problem of errors raised in slots is to determine that at the connection and not the emitting. Then the erros can be handled in a loosely coupled way.
class ExceptionTestWidget(QtGui.QWidget):
error = QtCore.Signal(object)
def abort_execution():
pass
def error_handler(self, err):
self.error.emit(error)
self.abort_execution()
(...)
def connect_with_async_error_handler(sig, slot, error_handler, *args,
conn_type=None, **kwargs):
#functools.wraps(slot)
def slot_with_error_handler(*args):
try:
slot(*args)
except Exception as err:
error_handler(err)
if conn_type is not None:
sig.connect(slot_with_error_handler, conn_type)
else:
sig.connect(slot_with_error_handler)
This way, we would be complying to the requirements in the Qt5 docs, stating that you need to handle exceptions within the slot being invoked.
Throwing an exception from a slot invoked by Qt's signal-slot
connection mechanism is considered undefined behaviour, unless it is
handled within the slot
PS: This is just a sugestion based on a very small overview of your use case. There is no right/wrong way of solving this, I just wanted to bring out a different point of view : )

Saving exceptions in Tkinter using Python

I have developed several Python programs for others that use Tkinter to receive input from the user. In order to keep things simple and user-friendly, the command line or python console are never brought up (ie. .pyw files are used), so I'm looking into using the logging library to write error text to a file when an exception occurs. However, I'm having difficulty getting it to actually catch the exceptions. For example:
We write a function that will cause an error:
def cause_an_error():
a = 3/0
Now we try to log the error normally:
import logging
logging.basicConfig(filename='errors.log', level=logging.ERROR)
try:
cause_an_error()
except:
logging.exception('simple-exception')
As expected, the program errors, and logging writes the error to errors.log. Nothing appears in the console. However, there is a different result when we implement a Tkinter interface, like so:
import logging
import Tkinter
logging.basicConfig(filename='errors.log', level=logging.ERROR)
try:
root = Tkinter.Tk()
Tkinter.Button(root, text='Test', command=cause_an_error).pack()
root.mainloop()
except:
logging.exception('simple-exception')
In this case, pressing the button in the Tkinter window causes the error. However, this time, nothing is written to the file, and the following error appears in the console:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python27\lib\lib-tk\Tkinter.py", line 1536, in __call__
return self.func(*args)
File "C:/Users/samk/Documents/GitHub/sandbox/sandbox2.pyw", line 8, in cause_an_error
a = 3/0
ZeroDivisionError: integer division or modulo by zero
Is there a different way to catch and log this error?
It's not very well documented, but tkinter calls a method for exceptions that happen as the result of a callback. You can write your own method to do whatever you want by setting the attribute report_callback_exception on the root window.
For example:
import tkinter as tk
def handle_exception(exception, value, traceback):
print("Caught exception:", exception)
def raise_error():
raise Exception("Sad trombone!")
root = tk.Tk()
# setup custom exception handling
root.report_callback_exception=handle_exception
# create button that causes exception
b = tk.Button(root, text="Generate Exception", command=raise_error)
b.pack(padx=20, pady=20)
root.mainloop()
For reference:
http://epydoc.sourceforge.net/stdlib/Tkinter.Tk-class.html#report_callback_exception
Since this question was about logging and being explicit where the error is occurring, I will expand on Bryan (totally correct) answer.
First, you have to import some additional useful modules.
import logging
import functools # for the logging decorator
import traceback # for printing the traceback to the log
Then, you have to configure the logger and the logging function:
logging.basicConfig(filename='/full/path/to_your/app.log',
filemode='w',
level=logging.INFO,
format='%(levelname)s: %(message)s')
def log(func):
# logging decorator
#functools.wraps(func)
def wrapper_log(*args, **kwargs):
msg = ' '.join(map(str, args))
level = kwargs.get('level', logging.INFO)
if level == logging.INFO:
logging.info(msg)
elif level == logging.ERROR:
logging.exception(msg)
traceback.print_exc()
return wrapper_log
#log
def print_to_log(s):
pass
Note that the function doing the logging is actually log and you can use it for both printing errors and regular info into your log file. How you use it: with the function print_to_log decorated with log. And instead of pass you can put some regular print(), so that you save the message to the log and print it to the console. You can replace pass with whatever commands you prefer.
Note nb. 2 we use the traceback in the log function to track where exactly your code generated an error.
To handle the exception, you do as in the already accepted answer. The addition is that you pass the message (i.e. the traceback) to the print_to_log function:
def handle_exception(exc_type, exc_value, exc_traceback):
# prints traceback in the log
message = ''.join(traceback.format_exception(exc_type,
exc_value,
exc_traceback))
print_to_log(message)
Having configured all this, now you you tell your Tkinter app that it has to use the handle_exception function:
class App(tk.Tk):
# [the rest of your app code]
if __name__ == '__main__':
app = App()
app.report_callback_exception = handle_exception
app.mainloop()
Or alternatively, if you work with multiple classes, you can even instruct the class itself to use the andle_exception function:
class App(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.report_callback_exception = handle_exception
# [the rest of your app code]
if __name__ == '__main__':
app = App()
app.mainloop()
In this way, your app code looks lean, you don't need any try/except method, and you can log at info level whichever event in your app.

Create a Log Box with tkinter text Widget

I want to create a box were the user is informed of what the application is actually doing.
I created a Text Widget were to show the print statements that I wrote in key points of the applications, so that it could serve as a log box.
To do this, I redirected the stdout to a subclass of the widget itself "upgraded" with a write method as I saw here in another post.
This does indeed work, but I noticed a problem that makes the box almost useless.
If you run the code, you can see that the sentences appear all at once. More puzzling for me is that not only the sentences of the "wait2" functions
appear togheter, but even the print statement of the calling function, "wait1", is shown at the end of the process.
Why this behaviour? what can I do to see the statement shown in the box as they are executed?
from Tkinter import *
import sys
import time
root = Tk()
class addwritemethod(object):
def __init__(self, widget):
self.widget = widget
def write(self, string):
self.widget.configure(state="normal")
self.widget.insert("end", string)
self.widget.see("end")
self.widget.configure(state="disabled")
def wait1():
print "Can you see me?"
wait2()
def wait2():
for i in range(10):
time.sleep(5)
print "Long time no see!"
t_go = Button(root, text= "Start!", width =12, command = wait1)
t_go.pack(side = LEFT, padx = 20, pady = 20)
tlog = Text(root,wrap = "word")
tlog.pack(side="top", fill="both", expand=True)
tlog.configure(state="disabled")
sys.stdout = addwritemethod(tlog)
mainloop()
EDIT: I want to say thanks to the people who answered me and an apology: I did not give all the required information.
I put time.sleep() in the test code only to show you the behaviour. In the real application, I trasfer a file via ssh with Paramiko and I don't use sleep().
Maybe I choose the wrong example, but the result is the same, all the print stament are shown at the same moment.
You could also use the built-in logging module to achieve your goal of
creating a box where the user is informed of what the application is actually doing ... a log box.
I had this same need and converged on the recommendations provided here and here.
I have an example below that I created to illustrate the concept of logging to a GUI control using Tkinter. The example below logs to a text control as you ask, but you can send log messages to other GUI components by replacing/copying the class MyHandlerText with other handler classes like MyHandlerLabel, MyHandlerListbox, etc. (choose your own names for the handler classes). Then you'd have a handler for a variety of GUI controls of interest. The big "a-ha" moment for me was the module-level getLogger concept encouraged by python.org.
import Tkinter
import logging
import datetime
# this item "module_logger" is visible only in this module,
# (but you can create references to the same logger object from other modules
# by calling getLogger with an argument equal to the name of this module)
# this way, you can share or isolate loggers as desired across modules and across threads
# ...so it is module-level logging and it takes the name of this module (by using __name__)
# recommended per https://docs.python.org/2/library/logging.html
module_logger = logging.getLogger(__name__)
class simpleapp_tk(Tkinter.Tk):
def __init__(self,parent):
Tkinter.Tk.__init__(self,parent)
self.parent = parent
self.grid()
self.mybutton = Tkinter.Button(self, text="ClickMe")
self.mybutton.grid(column=0,row=0,sticky='EW')
self.mybutton.bind("<ButtonRelease-1>", self.button_callback)
self.mytext = Tkinter.Text(self, state="disabled")
self.mytext.grid(column=0, row=1)
def button_callback(self, event):
now = datetime.datetime.now()
module_logger.info(now)
class MyHandlerText(logging.StreamHandler):
def __init__(self, textctrl):
logging.StreamHandler.__init__(self) # initialize parent
self.textctrl = textctrl
def emit(self, record):
msg = self.format(record)
self.textctrl.config(state="normal")
self.textctrl.insert("end", msg + "\n")
self.flush()
self.textctrl.config(state="disabled")
if __name__ == "__main__":
# create Tk object instance
app = simpleapp_tk(None)
app.title('my application')
# setup logging handlers using the Tk instance created above
# the pattern below can be used in other threads...
# ...to allow other thread to send msgs to the gui
# in this example, we set up two handlers just for demonstration (you could add a fileHandler, etc)
stderrHandler = logging.StreamHandler() # no arguments => stderr
module_logger.addHandler(stderrHandler)
guiHandler = MyHandlerText(app.mytext)
module_logger.addHandler(guiHandler)
module_logger.setLevel(logging.INFO)
module_logger.info("from main")
# start Tk
app.mainloop()
When you call sleep, the application does exactly that: it sleeps. When it's sleeping it can't update the display. As a general rule you should never call sleep in a GUI.
That being said, a quick fix is to make sure you call update after printing something to the log, so that Tkinter has a chance to update the screen. Add self.widget.update_idletasks() at the end of write (redrawing the screen is considered an "idle task").
This isn't a proper fix but it's good enough to illustrate why the data isn't appearing. A proper fix involves not calling sleep. There are many examples on stackoverflow related to this, and almost all of them involve using the after method.

Ending the GTK+ main loop in an Python MDI application

I am trying to code an application that consists of various windows (e.g., generic message dialog, login dialog, main interface, etc.) and am having trouble getting the gtk.main_quit function to be called: either I get a complaint about the call being outside the main loop, or the function doesn't get called at all.
I am a newbie to both Python and GTK+, but my best guess as to how to get this to work is to have a "root" window, which is just a placeholder that is never seen, but controls the application's GTK+ loop. My code, so far, is as follows:
import pygtk
pygtk.require("2.0")
import gtk
class App(gtk.Window):
_exitStatus = 0
# Generic message box
def msg(self, title, text, type = gtk.MESSAGE_INFO, buttons = gtk.BUTTONS_OK):
# Must always have a button
if buttons == gtk.BUTTONS_NONE:
buttons = gtk.BUTTONS_OK
dialog = gtk.MessageDialog(None, 0, type, buttons, title)
dialog.set_title(title)
dialog.set_geometry_hints(min_width = 300)
dialog.set_resizable(False)
dialog.set_deletable(False)
dialog.set_position(gtk.WIN_POS_CENTER)
dialog.set_modal(True)
dialog.format_secondary_text(text)
response = dialog.run()
dialog.destroy()
return response
def nuke(self, widget, data):
gtk.main_quit()
exit(self._exitStatus)
def __init__(self):
super(App, self).__init__()
self.connect('destroy', self.nuke)
try:
raise Exception()
except:
self.msg('OMFG!', 'WTF just happened!?', gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE)
self._exitStatus = 1
self.destroy()
if self.msg('OK', 'Everything worked fine') == gtk.RESPONSE_OK:
self.destroy()
# Let's go!
App()
gtk.main()
The nuke function never gets called, despite the explicit calls to destroy.
DIFF On #DonQuestion's advice:
- self.destroy()
+ self.emit('destroy')
- App()
+ app = App()
This didn't solve the problem...
UPDATE Accepted #jku's answer, but also see my own answer for extra information...
First, there is a bit of a test problem with the code: You call Gtk.main_quit() from the App initialization: this happens before main loop is even running so signals probably won't work.
Second, you'll probably get a warning on destroy(): 'destroy' handler only takes two arguments (self plus one) but yours has three...
Also with regards to your comment about control flow: You don't need a Window to get signals as they're a GObject feature. And for your testing needs you could write a App.test_except() function and use glib.idle_add (self.test_except) in the object initialization -- this way test_except() is called when main loop is running.
I think #jku's answer identifies my key error, so I have marked it accepted, but while playing around, I found that the MessageDialog does not need to run within the GTK+ loop. I don't know if this is as designed, but it works! So, I broke my generic message dialog out into its own function and then kept the main app altogether in a class of its own, which respects the main loop as I was expecting:
import pygtk
pygtk.require("2.0")
import gtk
def msg(title, text, type = gtk.MESSAGE_INFO, buttons = gtk.BUTTONS_OK):
# Only allowed OK, Close, Cancel, Yes/No and OK/Cancel buttons
# Otherwise, default to just OK
if buttons not in [gtk.BUTTONS_OK, gtk.BUTTONS_CLOSE, gtk.BUTTONS_CANCEL, gtk.BUTTONS_YES_NO, gtk.BUTTONS_OK_CANCEL]:
buttons = gtk.BUTTONS_OK
dialog = gtk.MessageDialog(None, 0, type, buttons, title)
dialog.set_title(title)
dialog.set_geometry_hints(min_width = 300)
dialog.set_resizable(False)
dialog.set_deletable(False)
dialog.set_position(gtk.WIN_POS_CENTER)
dialog.set_modal(True)
dialog.format_secondary_text(text)
response = dialog.run()
dialog.destroy()
return response
class App:
def __init__(self):
# Build UI
# Connect signals
# Show whatever
def appQuit(self, widget):
gtk.main_quit()
def signalHandler(self, widget, data = None):
# Handle signal
# We can call msg here, when the main loop is running
# Load some resource
# We can call msg here, despite not having invoked the main loop
try:
# Load resource
except:
msg('OMFG!', 'WTF just happened!?', gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE)
exit(1)
# n.b., Calls to msg work even without the following code
App()
gtk.main()
exit(0)

how many classes in a .PY file

I'm learning Python and WxPython. So far, I'm following the examples in AnotherTutorial, which is mainly wxPython related.
I'm trying to understand the following code:
import wx
class MyMenu(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, wx.DefaultPosition, wx.Size(380, 250))
menubar = wx.MenuBar()
file = wx.Menu()
edit = wx.Menu()
help = wx.Menu()
file.Append(101, '&Open', 'Open a new document')
file.Append(102, '&Save', 'Save the document')
file.AppendSeparator()
quit = wx.MenuItem(file, 105, '&Quit\tCtrl+Q', 'Quit the Application')
quit.SetBitmap(wx.Image('stock_exit-16.png',wx.BITMAP_TYPE_PNG).ConvertToBitmap())
file.AppendItem(quit)
edit.Append(201, 'check item1', '', wx.ITEM_CHECK)
edit.Append(202, 'check item2', kind=wx.ITEM_CHECK)
submenu = wx.Menu()
submenu.Append(301, 'radio item1', kind=wx.ITEM_RADIO)
submenu.Append(302, 'radio item2', kind=wx.ITEM_RADIO)
submenu.Append(303, 'radio item3', kind=wx.ITEM_RADIO)
edit.AppendMenu(203, 'submenu', submenu)
menubar.Append(file, '&File')
menubar.Append(edit, '&Edit')
menubar.Append(help, '&Help')
self.SetMenuBar(menubar)
self.Centre()
self.Bind(wx.EVT_MENU, self.OnQuit, id=105)
def OnQuit(self, event):
self.Close()
class MyApp(wx.App):
def OnInit(self):
frame = MyMenu(None, -1, 'menu2.py')
frame.Show(True)
return True
app = MyApp(0)
app.MainLoop()
It's mainly line 38 onwards that confuses me, as it defines another class MyApp. Should this not be a separate .PY module of type CLASS which then instantiates the MyMenu class? I'm using Eclipse, typed the entire code as is and tired to run it but got the error messages
Traceback (most recent call last):
File "C:\ws2\sample2\simple_gui\menu2.py", line 54, in <module>
app = MyApp(0)
File "C:\Python27\lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 7981, in __init__
self._BootstrapApp()
File "C:\Python27\lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 7555, in _BootstrapApp
return _core_.PyApp__BootstrapApp(*args, **kwargs)
File "C:\ws2\sample2\simple_gui\menu2.py", line 50, in OnInit
frame = MyMenu(None, -1, 'menu2.py')
File "C:\ws2\sample2\simple_gui\menu2.py", line 36, in __init__
menubar.Append(menu_file,'&Edit')
File "C:\Python27\lib\site-packages\wx-2.8-msw-unicode\wx\_core.py", line 11320, in Append
return _core_.MenuBar_Append(*args, **kwargs)
wx._core.PyAssertionError: C++ assertion "!m_menuBar" failed at ..\..\src\common\menucmn.cpp(820) in wxMenuBase::Attach(): attaching menu twice?
Can anyone shed some light as to why there would be 2 classes in the same py module? (note i'm a noob so assume entry level knowledge of OOP).
I got it working on my machine. But I'm really trying to understand what the code is doing, especially the app=MyApp(0) line. What does this do? what is the Zero for, why not another number like 3, or text like "hello".
If you're used to Java, you know you can only have one public class per file, and it should have the same name as the file itself (sorry if this has changed; it was true when I used Java ;). This is so when javac is compiling your program it can find dependent but not-yet-compiled classes in other files.
Python actually does something very similar -- you have one module per file, and that module has the same name as the file. You don't notice because you don't explicitly declare the module, but if you do
foo.py:
class A:
pass
bar.py:
import foo
print(foo.A())
you can see that foo has automatically become a module.
So it's almost the same thing, just Python is not as class-centric as Java, so it uses a module as the main unit to break up files.
So to answer your question, yes, it's perfectly OK to have as many classes as you want in one file, because they're all in the same module.
Having one (Frame or Panel) class per file is typical of wxPython coding.
Normally you build your frame without giving any special functionality and then you subclass it for that in another module. This way, especially when you work with gui buiding tools like wxGlade, you can modify the gui or even add new widgets to it independly of the code.
The second App class in your file is also typical of wxPython where it is very common to enclose the application details separated.
That said, in Python you can have as many clases per module as you want. Wether you have 1 or 5 depends on the functional completness of the module and how functionaly related are those classes.
In any case, when the module grows to more than 500-1000 lines it could be a good idea to split its contents in different files.
Finally, as commented above your code works perfect for me in win7 64-bits with wxpython 2.8. (I just had to comment the Set Bitmap line because I had not that image)
Edit: You are right. When you instantiate the Application object, the MyMenu frame is created and then the application enters its MainLoop. The App(value) is used to direct the application output to some places:
App(0) <-> App(False) <-> App(redirect=False) -> output to shell (i.e stderr, stdout)
App(1) <-> App(True) <-> App(redirect=True) -> output to an application Frame
With App(1) if you set the parameter filename then you will sent the output to a file.
From wx.App.__doc__:
param redirect: Should sys.stdout and sys.stderr be
redirected? Defaults to True on Windows and Mac, False
otherwise. If filename is None then output will be
redirected to a window that pops up as needed. (You can
control what kind of window is created for the output by
resetting the class variable outputWindowClass to a
class of your choosing.)
The signature of wx.App:
wx.App(redirect=True, filename=None, useBestVisual=False, clearSigInt=True)

Categories

Resources