© W. David Ashley and Andrew Krause 2019
W. David Ashley and Andrew KrauseFoundations of PyGTK Developmenthttps://doi.org/10.1007/978-1-4842-4179-0_7

7. Python and GTK+

W. David Ashley1  and Andrew Krause2
(1)
AUSTIN, TX, USA
(2)
Leesburg, VA, USA
 

Now that you have a reasonable grasp of GTK+ and a number of simple widgets, it is time to move to the details of how Python and GTK+ work together. We also cover other Python aspects that will be useful for your projects, as well as some useful PGObject libraries.

Although this book is not a comprehensive guide to Python, we examine several topics used by GTK+ that are not usually covered by basic Python programming guides.

Arguments and Keyword Arguments

Keyword parameters and arguments are used throughout the GTK+ library to pass class instance property values from class to subclass to subclass, and so on. So let’s examine this phenomenon closely.

The most important thing to understand about GTK+ class properties is that they are implemented as Python properties in PyGTK. This means that a reference to a property class and methods should be replaced as a standard Python class and methods when accessed. The following example shows how to access the Gtk.Window property named title.
win = Gtk.Window()
title = win.props.title
The property can also be set using standard Python methods.
win = Gtk.Window()
win.props.title = "My Main Window"

Of course, the Gtk.Window class also supplies the get_title() and set_title() methods to perform the same tasks, but the shortcut Python methods also perform the same tasks. The choice as to which methods you use is entirely up to you.

Now that you understand that GTK+ properties are implemented as Python properties, we can move on to describing how to use and pass keyword arguments to classes. Let’s continue looking at Gtk.Window and how you create instances of that class. The class definition for Gtk.Window looks like this:
class Gtk.Window(Gtk.Bin):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
So, what are these *args and **kwargs arguments/parameters and what do they do? PyGTK uses this methodology to pass property names and values to class instances. When a class instance receives these arguments, it has the choice to use them, pass them on to the super class, or simply throw them away. Most of the time, it uses the ones that match the properties that the class defines. It then locates the corresponding value and assigns it to the corresponding property name. It does this task using code similar to the code in Listing 7-1.
class MyWindow(Gtk.Window):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for arg in argv:
            print("Another arg through *argv :",arg) for kw in keywords:
            print(kw, ":", keywords[kw])
Listing 7-1

Keyword Arguments

Not shown in the example are formal arguments. There is a required order for arguments: formal arguments must all appear first in the argument list, followed by all args arguments, and finally, by all the keyword arguments. The following example shows the formal declaration for how this must work.
function(formal_args, args, kwargs)
The following are calling statements that use formal arguments, variable arguments, and keyword arguments.
# function using formal and variable args def function1(title, modal, *args):
# calling function1
function1("My title", False, # variable args follow
          "Peaches", "and", "Cream")
function1("My Window", True) # Only formal args, no variable args
function1(True)              # Invalid!!! Missing one formal arg
# function using formal and keyword args def function2(title, modal, **kwargs)
# calling function2
function2("My title", True, parent=window, accept_focus=True) function2("My Window", False) # Only formal args, no keyword args
function2(parent=window)      # Invalid, no formal args
There are many other variations of these examples, but if you follow these three simple rules, you should have no problem coping with all the variations:
  • Formal arguments must all appear in the argument list first. If there are no formal arguments, then they can be absent from the argument list. There can be as many formal arguments as you need.

  • Variable arguments must all appear next in the argument list. If there are no variable arguments, then they can be absent from the argument list. There can be as many variable arguments as you need.

  • Keyword arguments must all appear last in the argument list. If there are no keyword arguments, then they can be absent from the argument list. There can be as many keyword arguments as you need.

PyGTK rarely uses formal arguments; it uses variable and keyword arguments almost exclusively. This makes it a little easier to cope with instantiating all the GTK+ classes. Just remember that GTK+ ignores any keyword arguments that are not also property names. This is very useful when you want to establish and manage your own properties.

Logging

Logging tracks events that happen when software runs. The software developer adds logging calls to their code to indicate that certain events have occurred. An event is described by a descriptive message that can optionally contain variable data (i.e., data that is potentially different for each occurrence of the event). Events also have an importance that the developer ascribes to the event; the importance can also be called the level or severity.

When to Use Logging

Logging provides a set of convenience functions for simple logging usage. These are debug(), info(), warning(), error(), and critical(). Table 7-1 describes when to use logging for common tasks and the best tool to use for each task.
Table 7-1

Logging Tasks

Task You Want to Perform

Best Tool for the Task

Display console output for ordinary use of a command-line script or program

print()

Report events that occur during normal operation of a program (e.g., status monitoring or fault investigation)

logging.info () (or logging.debug() for very detailed output for diagnostic purposes)

Issue a warning for a particular runtime event

logging.warning() if there is nothing the client application can do about the situation, but the event should still be noted

Report an error for a particular runtime event

Raise an exception

Report suppression of an error without raising an exception (e.g., error handler in a long-running server process)

logging.error(), logging.exception() or logging.critical() as appropriate for the specific error and application domain

The logging functions are named after the level or severity of the events that they track. The standard levels and their applicability are described in Table 7-2 (in increasing order of severity).
Table 7-2

Logging Levels

Level

When It’s Used

DEBUG

Detailed information, typically of interest only when diagnosing problems.

INFO

Confirmation that things are working as expected.

WARNING

An indication that something unexpected happened, or indicative of some problem in the near future (e.g., disk space low). The software is still working as expected.

ERROR

Due to a more serious problem, the software has not been able to perform a function.

CRITICAL

A serious error indicating that the program itself may be unable to continue running.

The default level is WARNING, which means that only events of this level and above will be tracked, unless the logging package is configured to do otherwise.

Events that are tracked can be handled in different ways. The simplest way of handling tracked events is to print them to the console. Another common way is to write them to a disk file.

Some Simple Examples

The following is a very simple example.
import logging
logging.warning('Watch out!') # will print a message to the console logging.info('I told you so') # will not print anything
If you type these lines into a script and run it, you see the following printed on the console.
WARNING:root:Watch out!

The INFO message doesn’t appear because the default level is WARNING. The printed message includes the indication of the level and the description of the event provided in the logging call (i.e., Watch out!). Don’t worry about the “root” part for now; it is explained later. The actual output can be formatted quite flexibly if you need that; formatting options are also explained later.

Logging to a File

Recording logging events in a file is a very common, so let’s look at that next. Be sure to try the following in a newly started Python interpreter; don’t just continue from the session described earlier.
import logging
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')
And now if we open the file and look at what we have, we should find the log messages.
DEBUG:root:This message should go to the log file INFO:root:So should this WARNING:root:And this, too

This example also shows how you can set the logging level, which acts as the threshold for tracking. In this case, because we set the threshold to DEBUG, all of the messages were printed.

If you want to set the logging level from a command-line option, such as
--log=INFO
and you have the value of the parameter passed for --log in a loglevel variable, you can use
getattr(logging, loglevel.upper())

to get the value, which you pass to basicConfig() via the level argument.

You may want to error check any user input value, perhaps as in the following example.
# assuming loglevel is bound to the string value obtained from the
# command line argument. Convert to upper case to allow the user to
# specify --log=DEBUG or --log=debug
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)

The call to basicConfig() should come before any calls to debug(), info(), and so forth. As it’s intended as a one-off simple configuration facility, only the first call actually does anything; subsequent calls are effectively no-ops.

If you run the preceding script several times, the messages from successive runs are appended to the example.log file. If you want each run to start afresh, not remembering the messages from earlier runs, you can specify the filemode argument, by changing the call in the example to this:
logging.basicConfig(filename='example.log', filemode="w", level=logging.DEBUG)

The output is the same as before, but the log file is no longer appended, so the messages from earlier runs are lost.

Logging from Multiple Modules

If your program consists of multiple modules, the following is an example of how you could organize logging in it.
# myapp.py
# import logging
# import mylib
def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')
if __name__ == '__main__':
    main()
# mylib.py
import logging
def do_something():
logging.info('Doing something')
If you run myapp.py, you should see this in myapp.log:
INFO:root:Started INFO:root:Doing something INFO:root:Finished

This is hopefully what you were expecting to see. You can generalize this to multiple modules using the pattern in mylib.py. Note that for this simple usage pattern, apart from looking at the event description, you won’t know where in your application your messages came from by looking in the log file. If you want to track the location of your messages, you’ll need to refer to the documentation beyond this tutorial level.

Logging Variable Data

To log variable data, use a format string for the event description message and append the variable data as arguments; for example,
import logging
logging.warning('%s before you %s', 'Look', 'leap!')
displays
WARNING:root:Look before you leap!

As you can see, merging variable data into the event description message uses the old, %-style of string formatting. This is for backward compatibility; the logging package predates newer formatting options, such as str.format() and string.Template. These newer formatting options are supported, but exploring them is outside the scope of this book. See the Python documentation for more information.

Changing the Format of Displayed Messages

To change the format that is used to display messages, you need to specify the format you want to use.
import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')
This should print something like the following.
2010-12-12 11:41:42,612 is when this event was logged.
The default format for date/time display is ISO8601 or [RFC 3339]. If you need more control over the formatting of the date/time, provide a datefmt argument to basicConfig(), as follows.
import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S % logging.warning('is when this event was logged.')
This displays something like the following.
12/12/2010 11:46:36 AM is when this event was logged.

The format of the datefmt argument is the same as supported by time.strftime().

Exceptions

Running exceptions in GTK+, application are the same as running any standard Python program. Since the GTK module is simply a standard Python module that wraps the GTK+ APIs, the library implementation morphs all GTK+ exceptions into standard Python exceptions. The result of this is that you do not need to worry about catching Glib.Error errors. None will ever be thrown by the GTK module.

That does not mean that you can ignore standard Python exceptions. You should plan for any exceptions in your application, just the way you would for any Python application. Let’s review some principals of Python exceptions.

Raising Exceptions

Exceptions are raised automatically when something goes wrong in your application. Before we take a look at how to handle an exception, let’s take a look at how you can raise exceptions manually—and even create your own kinds of exceptions.

The Raise Statement

You raise an exception with the raise statement, which takes an argument that is either a class (which should subclass the Exception class) or an instance. When using a class as an argument, an instance of the class is automatically created. The following is an example of using the built-in Exception class.
>>> raise Exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception
> raise Exception('overload') Traceback (most recent call last):
  File "<stdin>", line 1, in <module> Exception: overload

The first example raises a generic exception with no information about what went wrong. The second example added the error message overload.

Many built-in classes are available. A full description of all the exception classes are available in the Python Library Reference in the “Built-in Exceptions” section. The following lists the class hierarchy for all the Python 3.x exceptions.
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |
           +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

Custom Exception Classes

There are a lot of built-in exceptions that cover a lot of ground. But there times when you might want to create your own exception class. For example, there is no GTK+ exception class, so you might have a need to create your own. This gives you a chance to selectively handle exceptions based on their class. Thus, if you wanted to handle GTK runtime errors, you would need a separate class for the exceptions.

You create such an exception just like you would any other class, but be sure to subclass the Exception class (either directly or indirectly, which means that subclassing any other built-in exception is okay). The following shows how to write a custom exception class.
class GtkCustomException)Exception): pass

Feel free to add your own methods to class as you need them.

Catching Exceptions

Of course, raising an exception is only the first part of exceptions. The really useful part is catching (or trapping) and handling exceptions in your own application code. You do this with the try and except statements. Let’s take a look at a simple example.
x = input('Enter the first number: ')
y = input('Enter the second number: ')
print(x/y)
This works nicely until the user enters zero as the second number.
Enter the first number: 10
Enter the second number: 0
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
ZeroDivisionError: division by zero
To catch the exception and perform some error handling (like printing a more friendly error message), you could rewrite the program like this:
try:
    x = input('Enter the first number: ')
    y = input('Enter the second number: ')
    print(x/y)
except ZeroDivisionError:
    print('The second number can not be zero!')

Although this solution might seem overblown for such a simple case, when hundreds of division statements are used throughout an application, this would be a more reasonable case.

Note

Exceptions propagate out of functions and methods to where they are called, and if they are not caught there either, the exceptions will “bubble up” to the top level of the program. This means that you can use try and except statements to catch exceptions that are raised in your own and other people’s code (modules, function, classes, etc.).

Raising and Reraising Exceptions

Exceptions can be raised inside other exceptions, passing the exception on to a higher level of code. To do this, the subsequent exception must be called without any arguments via the raise statement.

The following is an example of this very useful technique. The example passes the ZeroDivisionException to a higher level of code if the exception is not suppressed.
class SuppressedDivision:
    suppressed = False
    def calc(self, expr):
        try:
            return eval(expr)
        except ZeroDivisionError:
            if self.suppressed:
                print('Division by zero is illegal!')
            else:
                raise

As you can see, when the calculation is not suppressed, the ZeroDivisionException is caught but passed on to the higher level of code, where it will be caught and handled.

Catching Multiple Exceptions

The try and except block can also catch and process more than one exception. To see how this works, let’s enhance the previous example to catch the TypeError exception.
class SuppressedDivision:
    suppressed = False
    def calc(self, expr):
        try:
            return eval(expr)
        except ZeroDivisionError:
            if self.suppressed:
                print('Division by zero is illegal!')
            else:
                raise
        except TypeError:
            if self.suppressed:
                print('One of the operands was not a valid number!')
            else:
                raise

Now we begin to see the power of the try and except code block and using exceptions. In the preceding example, we are using the interpreter’s ability to examine all the variables of the calculation, instead of writing essentially the same code ourselves to process all the variables to find out if the calculation works before we process it.

We can also combine both exceptions into a single block of code, as follows.
class SuppressedDivision:
    suppressed = False
    def calc(self, expr):
        try:
            return eval(expr)
        except ZeroDivisionError, TypeError:
            if self.suppressed:
                print('One or both operands is illegal!')
            else:
                raise
We can also capture the object that causes the exception.
class SuppressedDivision:
    suppressed = False
    def calc(self, expr):
        try:
            return eval(expr)
        except (ZeroDivisionError, TypeError_, e:
            if self.suppressed:
                print('The value "' + str(e) '" is illegal!')
            else:
                raise

There is much more to processing exceptions but this information is enough to whet your appetite.

You should consult with your Python resources for more complete information on exceptions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.170.187