Wouldn’t it be great if everything worked exactly as it was supposed to and nothing ever went wrong? Sadly, life and programming don’t always work that way!
When writing an application, such as the HelloRPC_AddLandmark Python script in the last post, there are plenty of opportunities for things to go wrong. For example, programmers can make typos and users can enter alpha characters instead of numbers.
In the Python programming language, these errors are referred to as “exceptions”, and part of the coding process is to anticipate where things might go wrong and catch them before they cause a bigger problem, like crashing the application.
Try and Except
“Try…except” blocks of code are used to handle exceptions when they’re encountered, and Terragen RPC recommends catching certain kinds of exceptions. For example, it’s entirely possible for the user to run a Python script without first starting Terragen. As you might imagine, the script won’t find Terragen so it crashes, unless that exception is handled.
Let’s try to fabricate the above scenario, by picking up where we left off in Part 1 of the previous blog post. With the HelloRPC_AddLandmark.py script loaded into the IDLE editor, close the Terragen 4 Professional application. Now run the Python script.
As you can see in the image below, a lot of red text now appears in the IDLE Shell window, starting with the keyword “Traceback”, and concludes on the last line with the explanation “No connection could be made because the target machine actively refused it.”
This result is due to the fact that Terragen 4 Professional was not running when the Python script was executed, so it has effectively crashed. The last line also provides us with a very useful piece of information; the reason the exception was thrown, which was a “ConnectionRefusedError”. We’ll need that bit of information to catch the exception.
So then, how do “try…except” blocks capture and handle exceptions? There are four keywords or “clauses” to be aware of:
try: Defines the beginning of a block of code in which to capture and handle exceptions when they occur.
except: Defines the exception or error you’re looking for.
else: Code to run if an exception did not occur.
finally: Code that runs whether or not an exception occurred.
Using our example of Terragen 4 Professional not running when the Python script was executed, let’s examine how we could handle the exception.
The first line in red following the Traceback gives us a clue of where to start. It indicates that the problems started in line 4 of the Python script, which reads “project = tg.root()”. This is the first point in the program where we asked Terragen to provide information, so it’s the perfect place to start capturing the exceptions that are sure to follow.
To insert our “try…except” block, type the following line of code in the editor window right above line 4. The line numbers may change as you add lines of code.
Within a “block of code” Python requires some form of indentation after the first line in the block. By default, Python uses four spaces, so we’ll follow that guideline here. However, the number of spaces to indent a statement must be at least one, and they must be uniform within the block of code. You can use spaces or tabs to indent, but intermixing the two should be avoided because it can lead to errors as pictured below. It’s important to be aware of this when copy and pasting lines of code between different sources.
The “except” clause will follow the last line of code that instructed Terragen to do something, namely position the Landmark object at the origin of the project. Following that last line of code, type the “except” clause and the exception type to look for.
If we don’t tell the “except” clause to do something when the exception is encountered, we’ll get another Syntax error. For this example, we’ll simply use the print() function to display some text in the IDLE Shell window to indicate we caught the exception. Following the line of code just entered, indent a new line and type the following.
print ("Caught the exception.")
Be sure to also indent all the lines of code within the “try….except” block with four spaces, then run the program. Here’s what the program should look like and the output from the program when it is executed.
This time the program did not crash. It caught the exception and printed the text we told it to in the output window. So far so good, but there’s a better way to report the exception which will display the actual exception and any messages from Python.
Instead of just printing the generic text we previously told it to, we’ll capture the actual exception and assign it to the variable “e”; then print the variable as text using the str() function. We can also include a description within the print statement by enclosing text within quotes. We’ll connect our description and the text from the variable by using the “+” character. After modifying the program with the lines of code below, save and run it again. When the exception is encountered this time, the message printed to the IDLE Shell will contain more accurate information.
except ConnectionRefusedError as e: print("Terragen RPC error: " + str(e))
Let’s take a look at what the other two clauses can do as well.
The “else” clause is run if the specified exception does not occur. Let’s tell the program that if the “ConnectionRefusedError” is not encountered, to tell us everything is okay. Add these lines of code directly after the except clause block of code.
else: print("Everything's okay!")
Now, launch Terragen 4 Professional again so that it’s running, then save and run the Python program. This time the output in the IDLE Shell window indicates that everything’s okay because Terragen was running when the Python program was executed.
The “finally” clause will run whether or not an exception occurred. Let’s add the following lines of code to the program, then save and run the program once more.
finally: print ("This will be printed no matter what.")
As you can see in the image below, both the “else” and the “finally” clauses were run and output text to the IDLE Shell window.
Just for fun, close the Terragen application and rerun the Python program. This time, the “except” clause captured the error and output its message, and the “finally” clause did too.
We can capture more than one exception in this block of code by simply adding additional “except” statements to the program. When coding, it’s advisable to always try and capture the most specific exceptions first, and the more general exceptions later.
The Terragen RPC documentation recommends capturing three types of common exceptions; Python’s “ConnectionError”, which is a base-class exception and encompasses several connection related issues like the “ConnectionRefusedError” previously encountered, Python’s “TimeoutError”, and Terragen RPC’s own “terragen_rpc.ReplyError”.
ConnectionError – a built-in Python exception raised by the socket, which is one endpoint of a two-way communication link between two programs, for example the active Terragen project and terragen_rpc . This probably means that Terragen isn’t running or there is a misconfiguration in Terragen or the terragen_rpc module.
TimeoutError – A built-in Python exception raised either by the socket or by this module when a socket timeout occurs. This could mean that the current instance of Terragen failed to respond or that a previous instance has crashed without properly closing the socket.
tg.ReplyError – Terragen responded, but the data it returned could not be parsed.
As a programmer, you may not be able to do anything about these exceptions but they may be resolvable by the user at runtime, for example by restarting Terragen or fixing a network issue. For this reason, we want to keep the program running.
Let’s add these “except” clauses to our program. First, replace the “ConnectionRefusedError” exception with the “ConnectionError” base-class exception, and modify its print output as indicated below. Then add the remaining lines of code to capture the “TimeoutError” and “tg.ReplyError” exceptions.
except ConnectionError as e: print("Terragen RPC connection error: " + str(e)) except TimeoutError as e: print ("Terragen RPC timeout error: " + str(e)) except tg.ReplyError as e: print("Terragen RPC server reply error:" + str(e))
We no longer need the “else” and “finally” clauses, so for clarity within the program go ahead and delete those lines of code.
We caught the 3 exceptions above because we want to give the end user a chance to resolve the issue, for example by starting Terragen, and we didn’t want our program to terminate. There was nothing wrong with our program, so we, the programmer, didn’t need to see a traceback.
However, a captured tg.ApiError exception is slightly different because it may reveal a situation that should be fixed by the programmer, and if that’s the case then we want to know exactly where the error occurred in the code so we can fix it. On the other hand, it may be that our program was written correctly with one version of the API in mind, while the end user is running an older or newer version that’s incompatible in some way. In that case it might be better to let the user know, and let the program keep running. The problem is that we don’t know which of these cases is more applicable.
So we have 3 options:
A) Don’t catch the exception, and let the program terminate. This will print a traceback on the console and we’ll be able to use this information to see where we went wrong. This is good for us if we need to update our code, but it’s bad for the end user if our program terminates when it doesn’t really need to.
B) Catch the exception, and show an error to the user to help them solve the issue, but keep our program running. This is good for the end user in case they can update their version of Terragen to support whatever RPC calls our program is trying to make. However, this could make life difficult for us during development if we need to pinpoint where in our code the error occurred.
C) Catch the exception, and do something more interesting. For example, make the traceback information available to the programmer and end user, without terminating the program.
Let’s take a look at how to incorporate option “C” into our code. When the exception occurs, Python generates a traceback. We can capture the traceback data by importing the traceback() module into our program. Enter the following line of code just below the first import statement in our program.
Add to the program, one last except clause to handle a tg.ApiError exception when it is encountered. The “format_exe()” function which resides within the traceback module will return a string of data, which the print() function will then display wherever it is directed to; in our case the IDLE shell window.
except tg.ApiError: print("Terragen RPC API Error.") print (traceback.format_exc())
Now, when Terragen RPC raises an ApiError exception, our program will catch it and print the full traceback information, and the program won’t terminate.
Try and Except Code Block Example
The code snippet below, taken directly from the terragen_rpc module’s documentation, can be used as a template for exception handling in your own programs. Note, that in this example, the traceback module is imported into the program so that the traceback information can be printed.
import terragen_rpc as tg import traceback try: project = tg.root() # etc... except ConnectionError as e: # A built-in Python exception raised by the socket. # This probably means that Terragen isn't running or # there is a misconfiguration in Terragen or this client. print("Terragen RPC connection error: " + str(e)) # Extra handling here... # ... except TimeoutError as e: # A built-in Python exception raised either by the socket # or by this module when a socket timeout occurs. # This could mean that the current instance of Terragen failed # to respond or that a previous instance has crashed without # properly closing the socket. print("Terragen RPC timeout error: " + str(e)) # Extra handling here... # ... except tg.ReplyError as e: # The server responded, but the data it returned could not be parsed. print("Terragen RPC server reply error: " + str(e)) # Extra handling here... # ... except tg.ApiError: # The server responded indicating that there was an API misuse. If # using the High Level API this probably means the server is running # a version of the API that is incompatible with this particular call. # Exception subclasses include ApiMethodNotFound and ApiInvalidParams. print("Terragen RPC API error") # Extra handling here... # Let's print something useful for debugging. print(traceback.format_exc()) # requires import traceback # The program continues running after this.
There you have it, we’ve not only written our first Python program to take advantage of the Terragen RPC feature and add a Landmark object at the origin of the scene, but we’ve also considered what things might possibly go wrong and added error handling to circumvent program crashes.
These are very basic examples and meant to be an introduction to Terragen RPC and the Python programming language. You’ll find additional examples on the Github repository here, and in the online documentation here. Don’t forget to take a look at the Terragen RPC Time of Day tutorial here.