5
\$\begingroup\$

Yesterday, I've come up with the idea of using python context manager to ensure cleanup available here. This time I am using that context manager to make a decorator.


The context manager


# SafeGPIO.py
# updated, warning silenced

from RPi import GPIO
from exceptions import RuntimeWarning
import warnings

class SafeGPIO(object):

    def __enter__(self):
        return GPIO

    def __exit__(self, *args, **kwargs):
        with warnings.catch_warnings():
            warnings.simplefilter("error") #turn warning into exceptions
            try:
                GPIO.cleanup()
            except RuntimeWarning:
                pass # silence it

The decorator


#decorators.py

from . import SafeGPIO
from RPi import GPIO
from functools import wraps

def safe_gpio(func):
    """
    This decorator ensure GPIO.cleanup() is called when function call ends, 
    also it injects GPIO as first argument into your function
    """
    @wraps(func) # using wraps preservses doc string
    def wrapper(*args, **kwargs):
        with SafeGPIO() as GPIO:
            return func(GPIO, *args, **kwargs)
    return wrapper

def gpio(func):
    """
    This decorator injects GPIO as first argument into your function
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(GPIO, *args, **kwargs)
    return wrapper

Use like this:

# decorator_test.py

from SafeGPIO.decorators import safe_gpio, gpio
from time import sleep
from random import choice, randint

GPIO_PINS = (3,5,7,8,10,11,12,13,15,16,18,19,21,22,23,24,26)
VALUES = (True, False)

@safe_gpio
def do_random_things_with_pins_for_ten_times(GPIO):
    GPIO.setmode(GPIO.BOARD)
    for pin in GPIO_PINS:
        GPIO.setup(pin, GPIO.OUT)
    for _ in xrange(10):
        pin = choice(GPIO_PINS)      # choose one of the GPIO pin
        value = choice(VALUES)       # output either true or false
        sleep_seconds = randint(1,3) # sleep from 1 to 3 seconds
        print "slected pin %d, output %r, sleep for %d seconds" %
                (pin, value, sleep_seconds)
        GPIO.output(pin, value)
        sleep(sleep_seconds)

@safe_gpio
def do_real_work(GPIO):
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(7, GPIO.OUT)
    print "doing real work for 5 seconds"
    GPIO.output(7, True)
    sleep(5)

@safe_gpio # guarantee to clean up on exit
def main(GPIO):
    do_random_things_with_pins_for_ten_times()
    do_real_work()

if __name__ == '__main__':
    main()
\$\endgroup\$
2
  • \$\begingroup\$ Shouldn't it be @gpio around do_random_things_with_pins_for_ten_times and do_real_work, instead of @safe_gpio? \$\endgroup\$ – 301_Moved_Permanently Dec 11 '15 at 14:11
  • \$\begingroup\$ I want to clean up pins after do_random_things_with_pins_for_ten_times, as you don't know what the pins' states are after 10 iterations. As for do_real_work, it really depends on your use case. For main, in this case it really doesn't matter. \$\endgroup\$ – rabbit.aaron Dec 11 '15 at 14:48
2
\$\begingroup\$

My other answer says most of the stuff related to your underlying question of how to handle cleanup. In this answer I would like to focus on reviewing your code.

  • Join both versions in one file – I think I would like to have both of them in the same file, i.e. SafeGPIO, as that would allow for a slightly simpler structure, and the possibility to add a good description at top of file to properly highlight the options you present on how to do cleanup related to the GPIO.

  • Missing documentation on use cases within module file – I would very much appreciate to have the documentation on how to use it within the module itself, possibly with some links to explain why this is a focus area

  • Possible issues related to type of context manager - Using context manager can introduce issues related to single use, reusable and/or reentrant context managers, which needs some thinking. In your case, you might end up calling the GPIO.cleanup() multiple times, but there shouldn't be any harm in that. In the Technical details of Context Managers, some links are provided related to such issues.
  • Simplify the decorator – The decorator version can be simplified somwehat, using something like the following untested code:

    def cleanup_GPIO_at_end(func):
       """Decorator function executing func() followed by GPIO.cleanup()."""
    
       @wraps(func):
       def inner_wrapper(*args, **kwargs):
           return_value = func(*args, **kwargs)
           GPIO.cleanup()
           return return_value
    
       return inner_wrapper
    
  • Why the injection of GPIO as first argument? – I don't see the reasoning of that in your current code, so I'd leave it out, currently.

  • Consider at_exit() approach in addition to other means – I commented on this before, and also in my other answer.
  • Rename SafeGPIO to GPIO_cleanup – SafeGPIO doesn't convey what the class does, so I would rename it to GPIO_cleanup or CleanupGPIO to better indicate what happens in the background.

I've not commented upon the usage tests as I consider those test cases to illustrate usage of the cleanup variants. And they look nice enough, although I would probably use print as a function, and introduce a little vertical spacing (with comments above instead of after code).

Refactored code

Here is my suggestion for SafeGPIO.py:

"""
Documentation on how to use this module with use cases

...
"""
from RPi import GPIO
from exceptions import RuntimeWarning
import warnings
import atexit

class GPIO_cleanup(object):
    ## ... Copy code from SafeGPIO your post here ...

def GPIO_cleanup_at_end(func):
   """Decorator function executing func() followed by GPIO.cleanup()."""

   @wraps(func):
   def inner_wrapper(*args, **kwargs):
       return_value = func(*args, **kwargs)
       GPIO.cleanup()
       return return_value

   return inner_wrapper

# Flag to indicate whether to cleanup or not at module exit
disable_cleanup_at_exit = False

disable_cleanup_at_exit(disable = True):
    """Disable the default behaviour of cleaning up at exit of module."""
    disable_cleanup_at_exit = disable;


@atexit.register
def cleanup():
    if not disable_cleanup_at_exit:
        GPIO.cleanup()

This would enforce a default cleanup at end of script, unless abnormally terminated, and still allows for the use of with GPIO_cleanup as GPIO: or the decorator @GPIO_cleanup_at_end.

And final recommendation, is to use class encapsulation for a larger project to hide the actual pinout allowing for a better focus on the higher level functions of your script.

\$\endgroup\$
3
\$\begingroup\$

If you want to do gpio in one line, you can use functools.partial and either functools.wraps or functools.update_wrapper.

As you are using wraps already, you may as well use partial too.

def gpio(func):
    return update_wrapper(partial(func, GPIO), func)

Now it's a simple one-liner. If you don't need @wraps, this can be further simplified to:

def gpio(func):
    return partial(func, GPIO)

Ways I would change the program:

  • Adding an option to SafeGPIO to have the option to handle errors.

  • Merging gpio and safe_gpio into one function would improve my user experience.
    Less functions to remember, whilst keeping the option for it to be safe.

  • You don't use @gpio, so would you ever use it?

    If your response is no, you should consider removing it. As you could just import GPIO. And it seems like a lot of overhead for importing GPIO.

\$\endgroup\$
2
\$\begingroup\$

Alternative cleanup handlers

You want to ensure the calling of GPIO.cleanup, which seems to be recommended way of business for the Raspberry PI as it otherwise would leave the output pins in the current state, which could risk shortcuts later on if you reconnect a high pin to ground. See How to Exit GPIO programs cleanly, avoid warnings and protect your Pi.

This however is actually a general question on how to ensure calling of cleanup procedure when I'm done doing something. This has at least the following solutions:

  1. Manual handling – You can always call GPIO.cleanup() manually. Drawback of this is to remember doing it, and correctly do it when handling exceptions and other error situations

  2. Trigger at exit – Using at_exit will ensure that it is always called, unless abnormal termination of your script. Does however need a little tweaking if you by design want to leave pins in the high state.

  3. Using context managers – Allowing you to do with ... as a wrapper, like in OP code, before ending by doing the needed cleanup. For more on this see With Statements Context Managers

  4. Using decorators – The goal of cleaning up can also be met using decorators, also examplified by OP, where one can ensure the cleanup after the function finishes. See Decorators for Functions and methods (also see functools.wraps).

    I also found a non-official article, "Python Decorators II: Decorator Arguments", related to decorators with or without arguements, both class versions and functions versions.

  5. Through encapsulation and class destructors – When a class instantation is freed, there is a certain possibilty that the __del__ function is called. There are some caveats related to destructores, see "Safely using destructors in Python", related to circular reference counting and exception in constructors.

Why choose a specific handler

Your specific use cases will affect your choice of handler for cleaning up, and there can be large variations on needs and requirements. Here are some arguments related to the different options.

Manual handling

In the general case, using manual handling is unreliable as we're humans and we tend to forget to do the right thing. Having the option available though, is useful. The largest caveat for this is when exceptions and errors occur to then properly assure the cleanup actions to be taken.

Trigger at exit

Using a trigger at exit is, in my opinion, the safest option, as you then have made a conscious decision to always cleanup when script exits. This will catch all cases, except abnormal termination of the script, i.e. through external signals or similar. Exceptions doesn't not prevent the exit methods to be called.

However this option needs to have an option to actually allow cleanup not to take place if your script is use to handle a smart home where the pins needs to stay in their low or high position after script termination as you might want to use your Raspberry Pi to do other stuff in between. This does require proper handling of warnings when reconnecting on later invocations.

Context managers

Context manager does have their purpose and right of life, and it possibly comes down to personal choice whether to use these or not. They do have the element of remembering to use them, but when remembered they do the job.

One minor detail is that they increase indentation levels, and it can be hard to find the proper level of when to do it as the scope of this context might be rather large, spanning several functions.

And in my opinion I would like for a context manager to be in as narrow a scope as possible, without too many large functions called from it. That is to have at little code as possible within the scope of a context manager. Related to file handling, I would like for the file handler to be open for a shortest possible amount of time.

My opinion on this is most likely based on the concept of not clogging up resources, and releasing that at the earliest point of convenience. This might not apply though, as the requirement to cleaning up for GPIO is not a matter of resource allocation.

Using decorators

Decorators seems like a more natural choice to me, as they decorate a function and you can then give a proper bounding for when you need the cleanup to occur.

The concept of resource allocations doesn't apply as a decorator is by convention something to do in front or after functions, and I think this use of a decorator is a good choice.

Cleaning up through encapsulation

My last option is to use cleanup through encapsulation. That is to create a class which fetches or references the GPIO, and then when the object is freed it calls the cleanup action. At first this might seem like an extra step considered to other methods, but one advantage is that this could allow for proper encapsulation of more than just the cleanup.

It's not unlikely that you have a dedicated script to control a given set of pins, which lends it self to being encapsulated as a class where you hide the actual operation on the pins themselves, and instead provide helper methods like set_warning_led(state), engage_left_engine(state), ... That is you give your script higher level methods doing the logic, and not implicitly needing to do the GPIO.output(pin, value) with magic numbers.

This would allow for easier refactoring of pin layout, and a lot easier code to read at higher levels. And if doing this, it would also make sense to have the cleanup being a part of the class so that when freeing the class, the pins are cleaned up as well.

Conclusion

To be on the safe side I would implement the trigger at exit with an optional module method to disable the cleanup in the rare cases where you want to leave the pins in their current state. This would allow for an almost guaranteed cleanup unless you specific says to do otherwise.

Secondly I would use decorators given a low level script for intermediate cleanup of pins throughout various parts of script. Possibly opt'ing for the use of encapsulation for a larger system like if you build a smart home system, where it would make sense to encapsulate the different pins and have better names to address the different switches.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.