raspipe.py

The star-making machine is an interesting visualization of a pipeline, but maybe it would be better to use more of the available screen real-estate.

For something a bit different, try running some text through raspipe.py:

Download: file
shuf /usr/share/dict/words | head -500 | ./raspipe.py

With raspipe.py running, try clicking anywhere in the window and the display should switch to something a little more abstract:

You can click again to switch back to the text display.

The script itself reads a lot like machine_stars.py, but there are a few key differences. Open it up with Nano and let's have a look at some of the important ones (if you'd rather view it in a browser, you can find the code on GitHub):

Download: file
nano raspipe.py

Imports

The first thing you'll probably notice is that there're a couple of extra imports:

Download: file
#!/usr/bin/env python
import getopt
import re
import sys

import pygame

The re module, which you might remember from stars.py, will let us match on regular expressions. We use this to let the user optionally specify a pattern to highlight. More about that when we get to the penguin.

The getopt module will let us easily parse some command-line options.

RasPipe is a Class

Next, instead of just writing a series of procedural statements in order, I wrote RasPipe as a class. An outline of its structure looks something like this:

Download: file
class RasPipe:
    # Some properties are here.

    def __init__(self, infile):
        """Create a RasPipe object for a given input file."""
       
    def setfont(self, size):
        """Set a display font of a given size."""

    def wait_for_click(self):
        """Wait for the user to click on the screen (or quit)."""
          
    def toggle_stars(self):
        """Toggle whether we should display stars."""

    def scale_display(self):
       """Set the current font size and delay based on amount of input."""

    def penguin_think(self, thought):
        """Have a penguin appear and think a thought."""         

    def run(self):
        """Process standard input."""

What the Heck is a Class?

Python, like most modern languages, provides support for a technique known as Object Oriented Programming, often abbreviated OOP or OO.

OO can be a very deep topic, but at heart it isn't as complicated as it's often made out to be. The short version is that it offers a way to bundle together state (variables, often called properties when they're part of an object) with behavior (functions, often called methods when they're part of an object).

The RasPipe class describes an object. You can think of an object as a sort of box around a collection of variables (properties) and functions (methods). Here's a very simple Python class and some code which gets an object (an instance of this class) and calls one of its methods:

Download: file
#!/usr/bin/env python

class HelloSayer:
    name = ""

    def __init__(self, name):
        self.name = name 

    def speak(self):
        print("Hello " + self.name)

s = HelloSayer("Ralph")
s.speak()

def foo(param1, param2: is how you define a function in Python. The first parameter to object methods in Python is always the object instance itself. This is how objects refer to their own properties and call their own methods.

__init__() is a special method that gets called when we create an object instance with something like s = HelloSayer("Ralph").

RasPipe works just the same, with more properties and methods.

raspipe.py is a Utility and Takes Options

Most of what's in raspipe.py is a class definition, but if you look at the bottom you'll see a few lines that turn it into a standalone utility program and let it take some useful options:

Download: file
# Handle running this file as a standalone script.
if __name__ == '__main__':
    rp = RasPipe(sys.stdin)

    opts, args = getopt.getopt(sys.argv[1:], 'sx:y:')
    for opt, arg in opts:
        if opt == '-x':
            rp.size[0] = (int(arg))
        if opt == '-y':
            rp.size[1] = (int(arg))
        if opt == '-r':
            rp.grep_for = arg
        if opt == '-s':
            rp.toggle_stars()

    rp.run()

The if __name__ == '__main__': bit is a little magic that lets us check whether the current file is being run as the main Python program. (This way, other programs can include and reuse the RasPipe class for their own purposes - we'll get to that in Part 2.)

With rp = RasPipe(sys.stdin) we create an instance of RasPipe with self.infile set to sys.stdin. (To see everything that happens here, have a look at the __init__() defined at the top of the class.)

Next, we use the getopt module to see if the user passed in any command line arguments. This lets us do the following:

-s

Start with stars enabled instead of scrolling text.

-x 640

Start with a screen 640 pixels wide.

-y 480

Star with a screen 480 pixels tall.

-r '.*foo.*'

Have a penguin appear onscreen thinking any line that matches the regular expression .*foo.*.

We could easily add other options here. As an exercise, you might try adding a feature to change a property like bgcolor from the command line.

Finally, we call rp.run(), which is the method that does most of the heavy lifting.

RasPipe Has a Grepping Penguin

Suppose you have a lot of output streaming through raspipe.py, and want to highlight lines that match a particular pattern? Normally, you'd use a tool like grep to accomplish this.

Fortunately, Python supports its own variety of regular expressions, so it's pretty easy to give our program its own simple version of grep-like features. For example, if I wanted to check the output of dmesg for things about ethernet interfaces, I could do:

Download: file
dmesg | ./raspipe.py -r '.*eth0.*'

And see someting like the following:

This is done inside run() by checking each line to render against a regular expression:

Download: file
            for render_line in to_render:
                # Show a penguin thinking if we have a regular expression to
                # search for and found a match:
                if self.grep_for is not None:
                    if re.match(self.grep_for, render_line):
                        matched_line = render_line

And later calling a special method to draw the penguin and its thought:

Download: file
            if matched_line is not None:
                self.penguin_think(matched_line)

That function looks like this:

Download: file
    def penguin_think(self, thought):
        """Have a penguin appear and think a thought."""
        r = self.penguin.get_rect(left=50, bottom=self.height)
        self.screen.fill(self.fgcolor, [0, 10, self.width, r.top])
        self.screen.blit(self.penguin, r)
        thought_surface = self.font.render(thought, True, self.bgcolor)
        thought_r = thought_surface.get_rect(left=5, top=30)
        self.screen.blit(thought_surface, thought_r)
        pygame.display.update()

RasPipe Handles Mouse Clicks

Pygame offers a way to process all sorts of user input.

In this case, we only really deal with one interaction: When the user clicks on the screen, we toggle the display between a scrolling-text mode and a mode that displays little star images. To see how this is done, have a look at the first few lines of run():

Download: file
   def run(self):
        """Process standard input."""
        line = self.infile.readline()
        while line:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                if event.type == pygame.MOUSEBUTTONDOWN:
                    self.toggle_stars() 

Just like in machine_stars.py, we're looping over the contents of standard input. Here, however, we also check the Pygame event loop and make a decision if there's been a mouse click (or if the window has been closed, which will give us a pygame.QUIT).

RasPipe Plays Sound

You may have noticed this bit in __init()__:

Download: file
        # A little bit of sound.
        pygame.mixer.init()
        self.click = pygame.mixer.Sound('./tick.wav')

...and later on in run():

Download: file
            if self.tick % self.display_lines == 0:
                self.click.play()

This just plays a clicking noise every so often. Pygame's mixer can be used to do all sorts of things with individual sound files, and pygame.mixer.music allows for a custom soundtrack.

This guide was first published on Mar 20, 2015. It was last updated on Mar 20, 2015. This page (raspipe.py) was last updated on Apr 23, 2019.