Debugging IronPython Applications

0
240

Understanding IronPython Warnings

Warnings are simply indicators that something could be wrong with your application or might not work under all conditions. For example, if you use a deprecated (outdated) function, you might later find that the application refuses to work on all machines. You can use warnings for all kinds of purposes, including providing debugging messages for your application.

The main difference between a warning and an exception is that a warning won’t stop the application. When the interpreter encounters a warning, it outputs the warning information to the standard error device unless the interpreter is ignoring the warning. In some cases, you need to tell the interpreter to ignore a warning because the warning is due to a bug in someone else’s code, a known issue that you can’t fix, or simply something that is obscuring other potential errors in your code. A standard warning looks like this:

[code]
__main__:1: UserWarning: deprecated
[/code]

The elements are separated by colons (:) and each warning message contains the following elements (unless you change the message formatting to meet a specific need).

  • Function name (such as __main__)
  • Line number where the warning appears
  • Warning category
  • Message

You’ll discover more about these elements as the chapter progresses. In the meantime, it’s also important to know that you can issue warnings, filter them, change the message formatting, and perform other tasks using the warning-related functions shown in Table 12-1. You see these functions in action in the sections that follow.

Warning-Related Functions and Their Purpose
Table 12-1: Warning-Related Functions and Their Purpose
Warning-Related Functions and Their Purpose
Table 12-1: Warning-Related Functions and Their Purpose (Continue)

Working with Actions

Before you do too much with warnings, it’s important to know that warnings have an action associated with them. For example, you can choose to turn a particular warning into an exception or to ignore it completely. You can apply actions to warnings in a number of ways using either the filterwarnings() or simplefilter() function. Table 12-2 shows the list of standard warning actions.

Standard Warning Actions
Table 12-2: Standard Warning Actions
Table 12-2 (continued)
Table 12-2 (continued)

It’s important to work with a few warnings to see how filtering works because filters are exceptionally important. In order to use warnings, you import the warnings module. Figure 12-1 shows a typical instance of the default action. Notice that the first time the code issues the warnings .warn(“deprecated“, DeprecationWarning) warning, the interpreter displays a message. (Don’t worry too much about the specific arguments for the warnings.warn()function for right now; you see them explained in the “Working with Messages” and “Working with Categories” sections of the chapter.) However, the interpreter ignores the same warning the second time. If you change the message, however, the interpreter displays another message.

The default action displays each message just one time.
Figure 12-1: The default action displays each message just one time.

Of course, you could always associate a different action with the warnings .warn(“deprecated“, DeprecationWarning) warning. To make this change, you can use the simplefilter() function as shown in Figure 12-2. Now when you issue the warning, it appears every time.

You can set the warning to appear every time.
Figure 12-2: You can set the warning to appear every time.

Unfortunately, as shown in the figure, the change affects every message. Using the simplefilter() function affects every message in every module for a particular message category. Both the newmessage and deprecated messages always appear. Let’s say you want to make just the deprecated message always appear. To perform this task, you use the filterwarnings() function as shown in Figure 12-3 (after first resetting the category using the resetwarnings() function).

Use the filterwarnings() function when you need better control over filtering.
Figure 12-3: Use the filterwarnings() function when you need better control over filtering.

In this case, the warnings.warn(“deprecated“, DeprecationWarning) warning appears every time because its action is set to always. However, the warnings .warn(“newmessage“, DeprecationWarning) warning appears only once because it uses the default action.

You can also set an action at the command line using the –W command line switch. For example, to set the interpreter to always display warning messages, you’d use the –W always command line switch. The –W command line switch accepts an action, message, category, module, or line number (lineno) as input. You can include as many –W command line switches as needed on the command line to filter the warning messages.

The resetwarnings() function affects every warning category and every message in every module. You might not want to reset an entire filtering configuration by using the resetwarnings() function. In this case, simply use the filterwarnings() or simplefilter() function to set the warning back to the default action.

At this point, you might wonder how to obtain a list of the filters you’ve defined. For that matter, you don’t even know if there are default filters that the interpreter defines for you. Fortunately, the warnings class provides two attributes, default_action and filters, which provide this information to you. Listing 12-1 shows how to use these two attributes.

Listin g 12-1: Discovering the default action and installed filters

[code]
# Import the required modules.
import warnings
# Display the default action.
print ‘Default action:’, warnings.default_action
# Display the default filters.
print ‘nDefault Filters:’
for filter in warnings.filters:
print ‘Action:’, filter[0],
print ‘Msg:’, filter[1],
print ‘Cat:’, str(filter[2]).split(“‘“)[1].split(‘.’)[1],
print ‘Module:’, filter[3],
print ‘Line:’, filter[4]
# Add new filters.
warnings.filterwarnings(‘always’, message=’Test’, category=UserWarning)
warnings.filterwarnings(‘always’, message=’Test2’, category=UserWarning,
module=’Test’)
warnings.filterwarnings(‘always’, message=’Test3’, category=UserWarning,
module=’Test’, append=True)
# Display the updated filters.
print ‘nUpdated Filters:’
for filter in warnings.filters:
print ‘Action:’, filter[0],
try:
print ‘Msg:’, filter[1].pattern,
except AttributeError:
print ‘None’,
print ‘Cat:’, str(filter[2]).split(“‘“)[1].split(‘.’)[1],
try:
if len(filter[3].pattern) == 0:
print ‘Module: Undefined’,
else:
print ‘Module:’, filter[3].pattern,
except AttributeError:
print ‘Module: None’,
print ‘Line:’, filter[4]
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The code begins by importing the warnings module. It then displays (using warnings.default_ action) the default action that the interpreter will take when it encounters a warning. As shown in Figure 12-4 and described in Table 12-2, the default action is ‘default‘.

The example shows the default actions and filters, along with the output of filter changes.
Figure 12-4: The example shows the default actions and filters, along with the output of filter changes.

The next step is to show the default filters that the interpreter provides for you. It may surprise you to know that the interpreter does include some default filters for the PendingDeprecationWarning, ImportWarning, and BytesWarning, as shown in Figure 12-4. These default filters make the interpreter easier and more enjoyable to use, but could also hide important bugs, so you need to be aware of them.

In order to show how actions and filters work, the example adds three filters using the warnings .filterwarnings() function. The first filter simply tells the interpreter to always display warnings about the Test message provided in the UserWarning category. The second filter specifies that the Test2 warning will appear in the Test module. The third filter specifies that the interpreter should append the warning filter to the end of the filter list, rather than add it to the front of the list as is traditional. You can see the result of all three filter additions in Figure 12-4.

The code used to display the filter information is different in this case because the simple display method used earlier won’t work. What you’ll see as output for the message and module information is something like

[code]
<RE_Pattern object at 0x000000000000002C>
[/code]

which isn’t particularly useful. In order to get information from the message and module elements, you must access the pattern attribute. Unfortunately, this attribute isn’t available with the default filters, so the solution is to create a try…except AttributeError structure, as shown in the code. When the code encounters a default filter entry, it simply prints None as it would have done in the past.

Working with modules presents a special problem. If you look at the first filter declaration, it doesn’t include the Module attribute. Unfortunately, the interpreter takes this omission to mean that you want to create a blank entry, not a null entry. Consequently, the module code also handles the empty entry scenario by saying the module is undefined. If you want to create a null module entry, you must use Module=None as part of your filter declaration.

Notice in Figure 12-4 that the first two filters appear at the front of the list and in reverse order. That’s because the interpreter always adds new filters to the beginning of the list unless you include the append=True attribute. Because the third filter includes this attribute, it appears at the end of the list.

Working with Messages

A message is simply the text that you want to appear as part of the warning. The message is specific information about the warning so that someone viewing the warning will know precisely why the warning is issued. For example, if you issue a DeprecationWarning category warning, the output will automatically tell the viewer that something is deprecated. As a result, your message doesn’t have to tell the viewer that something is deprecated, but it does have to tell the viewer what is deprecated. In many cases, this means supplying the name of the feature such as a method name, attribute, function, or even a class.

Simply telling someone that a feature is deprecated usually isn’t enough information. At a minimum, you must include information about an alternative. For example, you might want to suggest another class or a different function. Even if there is no alternative, you should at least tell the viewer that there isn’t an alternative. Otherwise, the viewer is going to spend hours looking for something that doesn’t exist.

You can’t always tell someone why something is deprecated, but you should when you can. For example, it would be helpful to know that an old function is unstable and that the new function fixes this problem. It’s a good idea to extend this information by saying that the old function is supplied for backward compatibility (assuming that this really is the case).

In some cases, you also need to provide some idea of when a feature is deprecated, especially if the action occurs in the future. Perhaps your organization knows that a function is unstable but hasn’t come up with a fix yet. The fix will appear in the next version of a module as a new function. Having this information will help organizations that rely on your module to plan ahead for required updates.

The point of messages is that they should provide robust information — everything that someone needs to make good decisions. Of course, you don’t want to provide too much information either (anything over three well-written sentences is too much). If you feel the viewer needs additional information, you can always provide it as part of the feature’s help. That way, people who are curious can always find more information. Make sure you note the availability of additional information as part of your message.

Message consistency is another consideration. Remember that filters work with messages as well as categories and other warning elements. If two modules require the same message, make sure you use the same message to ensure filtering works as anticipated. In fact, copying and pasting the message is encouraged to reduce the risk of typographical errors.

If you ever want to see how your message will appear to others, you can use the formatwarning() function to perform the task. Try it out now. Open a copy of the IronPython console and try the following code.

[code]
import warnings
warnings.formatwarning(‘Bad Input’, UserWarning, ‘My.py’, 5, ‘import warnings’)
[/code]

You’ll see results similar to those shown in Figure 12-5. Notice that the output contains linefeeds like this: ‘My.py:5: UserWarning: Bad Inputn import warningsn‘. When you work with the printed version, the warning appears on multiple lines, as shown near the bottom of Figure 12-5.

Use formatwarning() to see how your warning will appear.
Figure 12-5: Use formatwarning() to see how your warning will appear.

Of course, it’s handy to know the arguments for the formatwarning() function. The following list provides a brief description of each argument.

  • Message: The message you want to display to the user.
  • Category: The warning category you want to use.
  • Filename: The name of the file where the warning occurred (not necessarily the current file).
  • Line number: In most cases, this value contains the line at which the warning is detected, which isn’t always the line at which the warning occurs. For example, it’s possible for a warning to appear at the end of a structure, rather than at the appropriate line within the structure.
  • Line of code: An optional entry that shows the line of code at which the warning occurs. If you don’t supply this argument, the formatwarnings() function defaults to a value of None. The IronPython implementation differs from the standard in this regard. According to the standard, the interpreter is supposed to read the file, obtain the correct line of code, and display the specified line when you don’t provide the appropriate text.

Working with Categories

A warning category is a means of identifying a particular kind of warning. The category makes it possible to group like warnings together and reduces the risk that someone will misinterpret the meaning of a message. In short, a category is a way to pigeonhole a particular message so that others know what you intend. Of course, filtering considers the warning category, so you also need to use the correct category to ensure filtering works as expected. Table 12-3 contains a list of the warning message categories, including a general Warning class that you shouldn’t ever use because it’s too general.

Warning Message Categories
Table 12-3: Warning Message Categories
Warning Message Categories
Table 12-3: Warning Message Categories (Continue)
Warning Message Categories
Table 12-3: Warning Message Categories (Continue)

The warning categories are used with almost every warnings module function. For example, you supply a category when setting a filter or creating a new message. There is always an exception. The resetwarnings() function doesn’t require any input, not even a warning category, because it resets the entire warning environment to a default state.

Obtaining Error Information

Errors will happen in your application, even if you use good exception handling. The handlers you create only react to the errors you know about. Applications also encounter unknown errors. In this case, your application has to have a way to obtain error information and display it to the user (or at least record it in a log file).

It’s important to remember that you normally obtain error information in an application using the exception process described. This section of the chapter is more designed for those situations where you need to work with a generic exception or obtain more detailed information than the specific exceptions provide.

As with many things, IronPython provides a number of methods for obtaining error information. In fact, you might be surprised at how many ways you can retrieve information once you really start looking. The following sections discuss the most common methods for obtaining error information.

Using the sys Module

The sys module contains a wealth of useful functions and attributes you use to obtain, track, and manage error information. One of the first things you should know about the sys module is that it contains the sys.stderr attribute, which defines where the interpreter sends error output. Normally, the output goes to the console window, but you can redirect the error output to any object that has a write() method associated with it, such as a file. If you want to later reset the sys.stderr attribute to the console, the sys.__stderr__ attribute always contains the original output location, so using sys.stderr = sys.__stderr__ performs a reset.

Obtaining error information seems like it should be straightforward, but it’s harder than most developers initially think because obtaining error information often affects application execution in unforeseen ways. In addition, ensuring that the caller receives the right information in a multithreaded application is difficult. The caller could also make unfortunate changes to error information objects, such as the traceback object, creating problems with circular references that the garbage collector is unable to handle. Consequently, you find a lot of functions in sys that look like they should do something useful (and this section covers them), but the two functions you need to keep in mind when working with IronPython are.

  • sys.exc_info(): Returns a tuple containing three items:
    • type: The type of the error, such as ZeroDivisionError. You can find a list of all standard exception types in the exceptions module.
    • value: The human readable string that defines the error. For example, a ZeroDivisionError might provide ZeroDivisionError(‘Attempted to divide by zero.‘,) as a value.
    • traceback: An object that describes the stack trace for an exception. Normally, you won’t use this information directly unless you truly need to obtain the stack trace information, which can prove difficult. If you need stack trace information, consider using the traceback module features instead
  • sys.exc_clear(): Clears the existing exceptions from the current thread. After you call this function, sys.exc_info() returns None for all three elements in the tuple.

The sys.exc_info() function isn’t very hard to use, but you can’t really try it out by executing it directly in the IronPython console. You need to place it within a try…except structure instead. The following code shows a quick demonstration you can type directly into the console window.

[code]
try:
5/0
except:
type, value = sys.exc_info()[:2]
print type
print value
[/code]

The example uses a simple division by zero to create an exception. As previously noted, you normally need just the first two elements of the tuple, which you can obtain using sys.exc_info()[:2]. When you execute this code, you see the following output.

[code]
<type ‘exceptions.ZeroDivisionError’>
Attempted to divide by zero.
[/code]

Some IronPython sys module functions affect only the interactive thread (which means they’re safe to use in multithreaded applications because there is only one interactive thread in any given session). You could use these functions to determine the current type, value, and traceback for an exception, but only for the interactive session, which means these functions are completely useless for your application. In most cases, you avoid using these three functions.

  • sys.last_traceback()
  • sys.last_type()
  • sys.last_value()

You could run into problems when working with some functions in the sys module. For example, these three functions are global, which means they aren’t specific to the current thread and are therefore, unsafe to use in a multithreaded application.

  • sys.exc_type()
  • sys.exc_value()
  • sys.exc_traceback()

Interestingly enough, these three functions are also listed as deprecated (outdated) in most Python implementations (including IronPython). As with all IronPython modules, you also have access to low-level functions in the sys module. The following list is low-level modules you can use for special needs, but won’t normally use in your application.

  • sys.excepthook(type, value, traceback): The system calls this low-level function each time it generates an exception. To use this function, you supply the same tuple of values as you receive when you call sys.exc_info().
  • sys._getframe([depth]): The system calls this low-level function to display a frame object from the call stack. If the caller supplies a depth value, the frame object is at that call stack depth. The default depth value setting is 0. IronPython doesn’t appear to implement this function, but you may encounter it in other versions of Python, so it pays to know about this function.

If you want to control how much information the interpreter provides when you request a traceback, you can always set the sys.tracebacklimit attribute. The sys.tracebacklimit attribute defaults to 1,000. It doesn’t actually appear when you perform a dir() command. In fact, until you set it, printing the sys.tracebacklimit attribute returns an AttributeError. Use code like this

[code]
sys.tracebacklimit = 3
[/code]

to modify the traceback level. Now when you try to print the sys.tracebacklimit attribute, you get back the value you supplied.

Using the traceback Module

The traceback module adds to the capabilities of the sys module described in the “Using the sys Module” section of the chapter. In addition, it adds to the standard exception handling capabilities of IronPython by making it easier to obtain complex information about exceptions in general. The traceback module does focus on tracebacks, which are the IronPython equivalent of a call stack.

The most common call is traceback.print_exc(). Essentially, this call prints out the current exception information. You can use it in a try…except structure, much as you’d use the sys.exc_info() function, but with fewer limitations. Figure 12-6 shows a typical view of the traceback.print_exc() function in action.

Obtain traceback information with ease using the traceback.print_exc() function.
Figure 12-6: Obtain traceback information with ease using the traceback.print_exc() function.

You may find that you want a string that you can manipulate, rather than direct output. In this case, you use the traceback.format_exc() function and place its output in a variable. The information is the same as shown in Figure 12-6, but you have the full capability of string manipulation functions to output the information in any form desired.

All of the traceback output functions include a level argument that defines how many levels of trace information you want. The default setting provides 1,000 levels, which may be a little more information than you want. Many of the traceback output functions also include a file argument that accepts the name of a file you can use for output (such as application logging). If you don’t provide the file argument, it defaults to using the sys.stderr device (normally the console).

Some of the traceback functions are macros for longer function combinations. For example, when you type traceback.print_last(), what you’re really doing is executing print_exception(sys.last_ type, sys.last_value, sys.last_traceback, limit, file). Obviously, typing traceback .print_last() is a lot less work!

IronPython is missing some extremely important functionality when it comes to the traceback module. You can’t use traceback.print_stack(), traceback.extract_stack(), or traceback.format_ stack() to obtain current stack information. The code shown in Figure 12-7 is standard output when working with Python. Figure 12-8 shows what happens when you execute this code in IronPython. Instead of getting a nice stack trace you can use for debugging (see Figure 12-7), you get nothing at all (see Figure 12-8). This is a known issue (see the issue information at http://ironpython.codeplex .com/WorkItem/View.aspx?WorkItemId=25543).

Python provides full stack information you can use for debugging.
Figure 12-7: Python provides full stack information you can use for debugging.
IronPython lacks support for stack traces, making debugging significantly more difficult.
Figure 12-8: IronPython lacks support for stack traces, making debugging significantly more difficult.

The traceback module contains a number of interesting functions that you can use to debug your application. You can see these functions described at http://docs.python.org/library/traceback .html. Don’t assume that all of these functions work as they do in Python. There are currently a number of outstanding traceback module issues for IronPython.

Debugging with the Python Debugger

You might not know it, but Python and IronPython come with a debugger module, pdb (for Python debugger). Like any module, you have full access to the debugger source code and can modify it as needed. This section describes the default debugger performance.

It’s possible to use pdb with any Python file by invoking the debugger at the command line using the –m command line switch. Here’s how you’d invoke it for the example shown in Listing 12-1.

[code]
IPY -m pdb ShowFilters.py
[/code]

Unfortunately, using this command line format limits what you can do with the debugger. Although you can single step through code, you can’t work with variables easily and some other debugger commands may not work as anticipated.

The debugger works better if you configure your application to use a main() module. Most of the examples in this book don’t use a main() function for the sake of simplicity, but you should use one for any production code you create. The ShowFilters2.py file contains the modifications to provide a main() function. Essentially, you encase the code in Listing 12-1 in the main() function and then call it using the following code:

[code]
# Create an entry point for debugging.
if __name__ == “__main__“:
main()
[/code]

Using the debugger is very much like old-style DOS debuggers such as the Debug utility. You issue commands and the debugger responds without output based on the application environment and variable content. The lack of a visual display may prove troublesome to developers who have never used a character-mode debugger, but pdb is actually more effective than any of the graphical alternatives in helping you locate problems with your application — at least, in the Python code. Use these steps to start the pdb:

  1. Start the IronPython console by selecting it from the Start menu or typing IPY at the command line.
  2. Type import pdb and press Enter to import the Python debugger.
  3. Type import ApplicationName where ApplicationName is the name of the file that contains your application and press Enter. For example, if your application appears in ShowFilters2.py, then you’d type import ShowFilters2 (without the file extension) and press Enter.
  4. Type pdb.run(‘ApplicationName.FunctionName()‘) where ApplicationName is the name of the application and FunctionName is the name of the function you want to test, and press Enter. For example, if your application is named ShowFilters2 and the function you want to test is main(), you’d type pdb.run(‘ShowFilters2.main()‘) and press Enter. The standard console prompt changes to a pdb prompt, as shown in Figure 12-9.
The Python debugger uses a special pdb prompt where you can enter debugging commands.
Figure 12-9: The Python debugger uses a special pdb prompt where you can enter debugging commands.

Now that you have a debugger prompt, you can begin debugging your application. Here is a list of standard debugger commands you can issue:

  • a or args: Displays the list of arguments supplied to the current function. If there aren’t any arguments, the call simply returns without displaying anything.
  • alias: Creates an alias for a complex command. For example, you might need to use a for loop to drill down into a list to see its contents. You could use an alias to create a command to perform that task without having to write the complete code every time. An alias can include replaceable variables, just as you would use for a batch file.
  • b or break: Defines a breakpoint when you supply a line number or a function name. When you provide a function name, the breakpoint appears at the first executable line within the function. If an application spans multiple files, you can specify a filename, followed by a colon, followed by a line number (no function name allowed), such as ShowFilters2:1. A breakpoint can also include a condition. To add the condition, follow the breakpoint specification with a comma and the condition you want to use, such as ShowFilters2:2, Filter == None. If you type just b or break, the debugger shows the current breakpoints. Use the cl or clear command to clear breakpoints you create.
  • bt, w, or where: Prints a stack trace with the most current frame at the bottom of the list. You can use this feature to see how the application arrived at the current point of execution.
  • c, cont, or continue: Continues application execution until the application ends or the debugger encounters a breakpoint.
  • cl or clear: Clears one or more breakpoints. You can specify the breakpoint to clear by providing one or more breakpoint numbers separated by spaces. As an alternative, you can supply a line number or a filename and line number combination (where the filename and line number are separated by a colon).
  • commands: Defines one or more commands that execute when the debugger arrives at a line of code specified by a breakpoint. You include the optional breakpoint as part of the commands command. If you don’t supply a breakpoint, then the commands command refers to the last breakpoint you set. To stop adding commands to a breakpoint, simply type end. If you want to remove the commands for a breakpoint, type commands, press Enter, type end, and press Enter again. A command can consist of any interactive Python or debugger command. For example, if you want to automatically move to the next line of code, you’d simply add step as one of the commands.
  • condition: Adds a condition to a breakpoint. You must supply a breakpoint number and a Boolean statement (in string format) as arguments. The debugger doesn’t honor a breakpoint with a condition unless the condition evaluates to True. The condition command lets you add a condition to a breakpoint after defining the breakpoint, rather than as part of defining the breakpoint. If you use condition with a breakpoint, but no condition, then the debugger removes a condition from a breakpoint, rather than adding one.
  • d or down: Moves the frame pointer down one level in the stack trace to a new frame.
  • debug: Enters a recursive debugger that helps you debug complex statements.
  • disable: Disables one or more breakpoints so that they still exist, but the debugger ignores them. You can separate multiple breakpoint numbers with spaces to disable a group of breakpoints at once.
  • enable: Enables one or more breakpoints so that the debugger responds to them. You can separate multiple breakpoint numbers with spaces to enable a group of breakpoints at once. Enabling a breakpoint doesn’t override any conditions that are set on the breakpoint. The condition must still evaluate to True before the debugger reacts to the breakpoint.
  • EOF: Tells the debugger to handle the End of File (EOF) as a command. Normally, this means ending the debugger session once the debugger reaches EOF.
  • exit or q or quit: Ends the debugging session. Make sure you type exit, and not exit(), which still ends the IronPython console session.
  • h or help: Displays information about the debugger. If you don’t provide an argument, help displays a list of available debugging commands. Adding an argument shows information about the specific debugging command.
  • ignore: Creates a condition where the debugger ignores a breakpoint a specific number of times. For example, you might want to debug a loop with a breakpoint set at a specific line of code within the breakpoint. You could use the ignore command to ignore the first five times through the loop and stop at the sixth. You must supply a breakpoint number and a count to use this command. The debugger automatically ignores the breakpoint until the count is 0.
  • j or jump: Forces the debugger to jump to the line of code specified as an argument.
  • l or list: Displays the specified lines of code. If you don’t supply any arguments with the command, the debugger displays 11 lines of code starting with the current line. When you supply just a starting point (a code line number), the debugger displays 11 lines of code starting with the starting point you specify. To control the listing completely, supply both a starting and ending point.
  • n or next: Continues execution to the next line of code. If the current line of code is a function, the debugger executes all of the code within the function and stops at the next line of code in the current function. In sum, this command works much like a step over command in most other debuggers.
  • p: Prints the value of an expression as the debugger sees it. Don’t confuse this command with the IronPython print() function, which prints an expression based on how IronPython sees it.
  • pp: Performs a pretty print. Essentially, this command is the same as the p command, except that the debugger interprets any control characters within the output so that the output appears with line feeds, carriage returns, tabs, and other formatting in place.
  • r or return: Continues execution until the current function returns. This command works much like a step out command in most other debuggers
  • restart: Restarts the current application at the beginning so that you can retest it. The command lets you supply optional arguments that appear as part of the sys.argv attribute. This command preserves debugger history, breakpoints, actions, and options.
  • run: Starts the application when used within Python as demonstrated earlier in this section. However, this command is simply an alias for restart when used within the debugger environment.
  • s or step: Executes the current line of code and then moves to the next line of code, even if that line of code appears within another function. This command works much like a step into command in most other debuggers
  • tbreak: Performs precisely like a break command, except that the debugger removes the breakpoint when the debugger stops at it the first time. This is a useful command when you want to execute a breakpoint just one time.
  • u or up: Moves the frame pointer up one level in the stack trace to an old frame.
  • unalias: Removes the specified alias (see the alias command for additional details).
  • unt or until: Continues execution until such time as the line number is greater than the current line number or the current frame returns. This command works much like a combination of the step over and step out commands in most other debuggers (see next, return, and step for other stepping commands).
  • whatis: Displays the type of the argument that you supply.

Debugging with the CLR Debugger

The CLR debugger, CLRDbg.EXE, is part of the .NET Framework SDK. You find it in the GuiDebug folder of your .NET Framework installation or in the Program FilesMicrosoft.NETSDKv2.0 GuiDebug folder. However, if you installed Visual Studio without installing the SDK, you might not see a GuiDebug folder. In this case, you can download and install the .NET Framework SDK separately. You can obtain the .NET Framework SDK for various platforms at these locations.

  • .NET Framework 2.0: http://msdn.microsoft.com/en-us/netframework/aa731542.aspx
  • .NET Framework 3.0: http://msdn.microsoft.com/en-us/netframework/bb264589.aspx
  • .NET Framework 3.5: http://msdn.microsoft.com/en-us/netframework/cc378097.aspx
  • .NET Framework 3.5 SP1: http://msdn.microsoft.com/en-us/netframework/ aa569263.aspx

This section relies on the CLRDbg.EXE version found in the .NET Framework 2.0 SDK. However, the instructions work fine for every other version of the CLR debugger as well. The newer versions of the debugger may include a few additional features that you won’t likely use or need when working with IronPython. The following steps describe how to start the debugger.

  1. Start the CLR debugger. If you installed the .NET Framework SDK separately, choose Start ➪ Programs ➪ Microsoft .NET Framework SDK v2.0 ➪ Tools ➪ Microsoft CLR Debugger. It’s also possible to start the CLR debugger from the command line by typing CLRDbg and pressing Enter as long as the debugger’s location appears in the path. You see the Microsoft CLR Debugger window.

    Provide the information needed to debug your application.
    Figure 12-10: Provide the information needed to debug your application.
  2. Choose Debug ➪ Program to Debug. You see the Program to Debug dialog box shown in Figure 12-10. This dialog box is where you enter the IronPython executable and script information, along with any command line switches you want to use.
  3. Click the ellipsis (…) in the Program field and use the Find Program to Debug dialog box to locate the IPY.EXE file. Click Open to add the IPY.EXE information to the dialog box.
  4. Type –D NameOfScript.py in the Arguments field (the example uses –D ShowFilters2 .py). Type any additional command line arguments you want to use while working with the application.
  5. Click the ellipses in the Working Directory field and use the Browse for Working Directory dialog box to locate the script directory (not the IPY.EXE directory). Click Open to select the working directory.
  6. Click OK. The CLR debugger prepares the debugging environment. However, you don’t see any files opened. You must open any files you wish to interact with as a separate step.
  7. Choose File ➪ Open ➪ File. Locate the source files you want to debug (ShowFilters2.py for the example). Click Open. You see the source file opened in the Microsoft CLR Debugger window. Figure 12-11 shows an example of how your display should look when working with the example. (The figure shows the debugger in debugging mode.)

    Open the source files you want to debug.
    Figure 12-11: Open the source files you want to debug.

At this point, you can begin working with the script just as you would with the Visual Studio debugger. The next section, “Using Visual Studio for IronPython Debugging,” discusses this debugger in more detail.

Using Visual Studio for IronPython Debugging

When you click Start Debugging, the debugger stops at the line of code as you might expect. Now, create a watch for both filter and filters. As shown in Figure 12-12, you can drill down into a complex object and examine it. In many cases, you must look through the Non-Public Members to find what you want, but the data is there for you to peruse. In this case, you can see all five elements in filters and even see the pattern data. Notice that the Type column is truly helpful in showing you which types to use when interacting with the data.

Watches let you drill down into both Python and .NET data.
Figure 12-12: Watches let you drill down into both Python and .NET data.

Unfortunately, Figure 12-12 also shows the other side of the coin. You can’t access warnings .filters even though it should be available. The Visual Studio debugger often produces poor results when working with Python-specific objects. If you have a need for working with these objects.

As shown in Figure 12-13, you can use the Immediate window to query objects directly. However, you can’t drill down into an object as you might have in the past. Consequently, entering ? filter works just fine, but entering ? filter[0] doesn’t.

The Immediate window is only partially useful when working with IronPython.
Figure 12-13: The Immediate window is only partially useful when working with IronPython.

In general, you’ll find that using the Python debugger works better for some Python-specific applications. Even though the Visual Studio debugger does provide a nice visual display, the quality of information isn’t quite as good. Of course, the picture changes when your application mixes Python and .NET code. In this case, the Visual Studio debugger can be your best friend because it knows how to work with the .NET objects.

 Defining and Using Exceptions

Exceptions are an essential part of any application. In fact, most developers have no problem using them at all. Unfortunately, many developers also misuse exceptions. Instead of providing robust code that handles common problems, the developer simply raises an exception and hopes someone else does something about the issue. Exceptions are generally used to address conditions that you couldn’t anticipate.

IronPython provides access to both Python exception and .NET exceptions, so the developer actually has twice as many opportunities to catch errors before they become a problem. It’s important to use the correct kind of exception handling. If you’re working with .NET code, you’ll normally use a .NET exception. Python exceptions address anything that isn’t .NET-specific. The following sections provide additional information about exceptions.

Implementing Python Exceptions

Python provides a number of standard exceptions, just as the .NET Framework does. You find these exceptions in the exceptions module. To see the list of standard exceptions, import the exceptions module and perform a dir() command on it, as shown in Figure 12-14.

Python stores its list of standard exceptions in the exceptions module.
Figure 12-14: Python stores its list of standard exceptions in the exceptions module.

The various exceptions provide different amounts of information. For example, when working with an IOError, you can access the errno, filename, message, and strerror attributes. On the other hand, a ZeroDivisionError provides only the message attribute. You can use the dir(exceptions .ExceptionName) command to obtain information about each of the exception attributes.

As with .NET, you can create custom exceptions using Python. The documentation for creating a custom exception is a bit sketchy, but you can create a custom exception (usually with the word Error in the name by convention) for every need. Listing 12-2 shows all of the Python exception basics, including creating a relatively flexible custom exception.

Listin g 12-2: Discovering the default action and installed filters

[code]
# Import the required modules.
import exceptions
# Define a custom exception.
class MyError(exceptions.Exception):
errno = 0
message = ‘Nothing’
def __init__(self, errno=0, message=’Nothing’):
self.errno = errno
self.message = message
def __str__(self):
return repr(self.message)
# Display the Error exception list.
for Error in dir(exceptions):
if ‘Error’ in Error:
print Error
# Create a standard exception.
try:
5/0
except ZeroDivisionError as (errinfo):
print “nDivide by Zero error: {0}“.format(errinfo)
# Create a custom exception.
try:
raise MyError(5, ‘Hello from MyError’)
except MyError, Info:
print “Custom Error({0}): {1}“.format(Info.errno, Info.message)
# Pause after the debug session.
raw_input(‘nPress any key to continue…’)
[/code]

The code begins by importing exceptions. The for loop lists all of the exceptions (the names of the types) found in exceptions, as shown in Figure 12-15. Notice how the code uses if ‘Error‘ in Error to locate just the exceptions in the module. This technique is useful for a lot of tasks in IronPython where you need to filter the output in some way.

The example shows basic exception handling and creation for Python.
Figure 12-15: The example shows basic exception handling and creation for Python.

The next bit of code raises a standard exception and then handles it. The output shows just a message. Notice that this exception relies on the as clause to access the error information.

It’s time to look at a custom exception, which begins with the MyError class definition. At a minimum, you should define both __init__() and __str__() or the exception won’t work as intended. Notice how __init__() assigns default values to both errno and message. You can’t depend on the caller to provide this information, so including default values is the best way to approach the problem. You can always assign other values later in the code based on the actual errors.

Make sure you create attributes for any amplifying information you want the caller to have. In this case, the example defines two attributes errno and message.

The __str__() method should return a human-readable message. You can return just the text portion of the exception or return some combination of exception attributes. The important thing is to return something that the developer will find useful should the exception occur. You can test this behavior out with the example by typing raise MyError. Here’s the output you’ll see.

[code]
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
__main__.MyError: ‘Nothing’
[/code]

Because you didn’t provide any arguments, the output shows the default values. Try various combinations to see how the output works. The example tries the exception in a try…except statement. Notice that a custom exception differs from a standard exception in that you don’t use the as clause and simply provide a comma with a variable (Info in this case) instead. You can then use the variable to access the exception attributes as shown. Figure 12-15 shows how the custom exception outputs information. Of course, your custom exception can provide any combination of values.

Implementing .NET Exceptions

In general, you want to avoid using .NET exceptions in your IronPython applications, except in those cases where you need to provide specific functionality for .NET code. The problem is that IronPython views such exceptions from a Python perspective. Consequently, trapping .NET exceptions can prove tricky unless you spend some time working with them in advance.

Many .NET exceptions are available in the System assembly so you need to import it before you can perform any serious work. After that, you can raise a .NET exception much as you do a Python exception. Handling the exception follows the same route as using a try…except statement. However, the problem is that the exception you get isn’t the exception you raised. Look at Figure 12-16 and you see that the ArgumentException becomes a ValueError and the ArithmeticException becomes an ArithmeticError.

Sloppy programming will cost you so much time as to make the programming experience a nightmare. Using a combination of warnings, error trapping, and exceptions will make your code significantly easier to debug. Of course, choosing the right debugging tool is also a requirement if you want to go home this weekend, rather than spending it in your office debugging your latest application.