LCD Manager – part 2: core

We know what we want. So lets get to work. We will work on core class Manager, base for all widgets and first widget, label.

I will show how to use some basic of TDD and we will write test, then do coding and go with refactor.

Download code.

Manager core

Lets start with manager. We want it to allow only buffered lcds and after init it must have empty dict for widgets. We will use TDD methodology. Create file in test directory test_manager.py and start with first test:


#!/usr/bin/python
# -*- coding: utf-8 -*-
#pylint: skip-file

"""Tests for charlcd.lcd"""


from nose.tools import assert_equal
from nose.tools import assert_raises
import charlcd.buffered as buffered
from charlcd.drivers.null import Null
from lcdmanager import manager

class TestManager(object):
    def test_correct_init(self):
        lcd = buffered.CharLCD(16,2, Null())
        lcd_manager = manager.Manager(lcd)
        assert_equal(lcd_manager.widgets, [])
        assert_equal(lcd_manager.name_widget, {})

Remember about tests file in root directory of project? No ? Than this is simple script that sets correct permissions for test files and run nosetests. I must use it because I work on shared drive and have wider permissions on files but nose require 0644.

Now we are in red state – if you run test, its destined to fail.

Lets start with code.  First we need to create basic skeleton. Create manager.py in lcdmanager package and paste:


#!/usr/bin/python
# -*- coding: utf-8 -*-

"""LCD Manager"""

import charlcd.abstract.lcd as lcd


class Manager(object):
    """Class Manager"""
    def __init__(self, lcd):
        if lcd.display_mode != lcd.DISPLAY_MODE_BUFFERED:
            raise AttributeError("lcd must be instance of buffered lcd")

        self.lcd = lcd
        self.widgets = []
        self.name_widget = {}

    def flush(self):
        """display content"""
        pass

    def add_widget(self, widget, name=None):
        """add widget to manager"""
        pass

    def get_widget(self, name):
        """get widget from  manager"""
        pass

Try running tests – they pass – we are green.

Add another test to check if manager will fail with lcd in direct mode.

def test_incorrect_init(self):
    lcd = direct.CharLCD(16,2, Null())
    assert_raises(AttributeError, manager.Manager, lcd)

Run tests – still green.

One important thing. I done small shortcut. If we follow TDD by word, first we wont write raise command. We would add it after second test.

Label demo – first steps

We need some code to see our work. Create file label.py in demos folder. We want to create label with text, add it to manager and display it on lcd. Start from basics:


#!/usr/bin/python
# -*- coding: utf-8 -*-

"""LCD Manager - label demo"""


import sys
sys.path.append("../../")
import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import buffered as buffered
from charlcd.drivers.gpio import Gpio
from charlcd.drivers.i2c import I2C
from lcdmanager import manager

GPIO.setmode(GPIO.BCM)


def label1():
    lcd = buffered.CharLCD(16, 2, I2C(0x20, 1))
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    

label1()


Widget abstract class

We would like to add label but first we need to write it 🙂 We need some skeleton for all widgets so lest go with abstract class. This abstract class require position, visibility and name. So we will write a test for it. Create file test_widget.py in tests folder and paste:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#pylint: skip-file

"""Tests for widget abstract class"""


from nose.tools import assert_equal
from nose.tools import assert_raises
import lcdmanager.abstract.widget as widget


class TestWidget(object):
    def setUp(self):
        self.widget = widget.Widget(12, 1, 'New', True)

    def test_correct_init(self):
        assert_equal(self.widget.position, {'x': 12, 'y': 1})

    def test_position(self):
        self.widget.position = {'x': 3, 'y': 4}
        assert_equal(self.widget.position, {'x': 3, 'y': 4})
        assert_raises(AttributeError, setattr, self.widget, 'position', 'abc')

Now we are in red state. Class widget does not exist.

Create file widget.py in abstract folder. And paste:


#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Widget - abstract class"""


class Widget(object):
    """Widget abstract class"""
    def __init__(self, pos_x, pos_y, name=None, visibility=True):
        self.position = {
            'x': pos_x,
            'y': pos_y
        }
        self.name = name
        self.visibility = visibility

    def render(self):
        """returns widget view array"""
        raise NotImplementedError("render not implemented")

When you look at description of render you will see magical word array. Why will we return array and not something else? Answer is simple. Widget may be displayed in few lines, each line goes to each dictionary row and manager will know what to do with it.

We are still in red. We need two more functions. So add:

    @property
    def position(self):
        """returns widget position"""
        return self._position

    @position.setter
    def position(self, pos):
        """sets widget position"""
        if not isinstance(pos, dict):
            raise AttributeError("dict with x and y required")
        if not pos['x'] or not pos['y']:
            raise AttributeError("x and y required")
        self._position = pos

And now we are green 🙂

Lets add visibility support. First test:

    def test_visibility(self):
        assert_equal(self.widget.visibility, True)
        self.widget.visibility = False
        assert_equal(self.widget.visibility, False)
        assert_raises(AttributeError, self.widget.visibility, 'abc')

and functions:

    @property
    def visibility(self):
        """return widget visibility"""
        return self._visibility;

    @visibility.setter
    def visibility(self, visibility):
        """sets widget visibility"""
        if (not isinstance(visibility, bool)):
            raise AttributeError("Boolean required")
        self._visibility = visibility

Same with name propety:

    def test_name_change(self):
         assert_equal(self.widget.name, 'New')
         self.widget.name = "Newer"
         assert_equal(self.widget.name, 'Newer')

Funny we are green 🙂

Label

Create new package in lcdmanager, package called widget. Now create test file test_label.py.

First we need to create base inits. As always in TDD, test goes first:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#pylint: skip-file

"""Tests for label class"""


from nose.tools import assert_equal
from nose.tools import assert_raises
import lcdmanager.widget.label as label


class TestLabel(object):
    def setUp(self):
        self.label = label.Label(12, 1, 'New', True)

    def test_correct_init(self):
        assert_equal(self.label.position, {'x': 12, 'y': 1})
        assert_equal(self.label.visibility, True)

Now we are in red state. Create label.py in widget package. And start with:

#!/usr/bin/python
# -*- coding: utf-8 -*-


"""Widget - label"""

import lcdmanager.abstract.widget as widget


class Label(widget.Widget):
    """Label widget"""
    def __init__(self, pos_x, pos_y, name=None, visibility=True):
        widget.Widget.__init__(self, pos_x, pos_y, name, visibility)

And we are green.
Write tests for most important functions in label – set and get label property.

    def test_label(self):
        self.label.label = "It's label"
        assert_equal(self.label.label, "It's label")

and functions:

     @property
    def label(self):
        """return label text"""
        return self._label

    @label.setter
    def label(self, label):
        """set label"""
        self._label = label

Finally we need to implement render function. So first test:

    def test_render(self):
        self.label.label = "It's label"
        assert_equal(self.label.render(), ["It's label"])

and function second:

    def render(self):
        """return view array"""
        return [self._label]

But what will happen when we call render without set label ? We should have empty array. Check it:

    def test_empty_render(self):
        assert_equal(self.label.render(), [""])

Run tests and.. test red, failed. When we use @property and .setter, we are using private variables but they are uninitialized! Something we forgot. Add:

self._label = ""

to label __init__, and we are green again.

Display a label

So we have basic label. It would be nice to add it to manager and display it. So back to Manager.

Lets start with add_widget. Its task is simple, add widget to internal array. So first test 🙂

    def test_should_add_label_without_name_to_widgets_dict(self):
        widget_label = label.Label(1, 1)
        widget_label.label = "name"
        self.lcd_manager.add_widget(widget_label)
        assert_equal(self.lcd_manager.widgets, [widget_label])

and code (see that we have removed name parameter from add_widget, we don’t need it).

    def add_widget(self, widget):
        """add widget to manager"""
        self.widgets.append(widget)

And next step with named label, tests:

    def test_should_add_label_with_name_to_widgets_dict(self):
        widget_label = label.Label(1, 1, 'text1')
        widget_label.label = "name"
        self.lcd_manager.add_widget(widget_label)
        assert_equal(self.lcd_manager.widgets, [widget_label])

    def test_should_get_widget_by_name(self):
        widget_label = label.Label(1, 1, 'text1')
        widget_label.label = "name"
        self.lcd_manager.add_widget(widget_label)
        assert_equal(self.lcd_manager.get_widget('text1'), widget_label)

And question is how we will find widget, we can loop over all items or use another dictionary as index name to id. I prefer another dictionary.

    def add_widget(self, widget):
        """add widget to manager"""
        self.widgets.append(widget)
        if widget.name is not None:
            self.name_widget[widget.name] = len(self.widgets) - 1

    def get_widget(self, name):
        """get widget from  manager or None"""
        if name in self.name_widget:
            return self.widgets[self.name_widget[name]]

        return None

And one more test just to have not found case tested:

    def test_should_not_get_widget_by_name(self):
        widget_label = label.Label(1, 1, 'text1')
        widget_label.label = "name"
        self.lcd_manager.add_widget(widget_label)
        assert_equal(self.lcd_manager.get_widget('text2'), None)

We are green and we have some more functionality. Good 🙂

To display our widget we need one more function: render. It should iterate over all widgets and write their view to lcd buffer. We will start with setUp change and new test:

    def setUp(self):
        self.lcd = buffered.CharLCD(20, 4, Null())
        self.lcd.init()
        self.lcd_manager = manager.Manager(self.lcd)
        self.lcd_buffer = [" ".ljust(20, " ") for i in range(0, 4)]

    def test_it_should_render_one_label(self):
        widget_label = label.Label(1, 1)
        widget_label.label = "name"
        self.lcd_manager.add_widget(widget_label)
        self.lcd_manager.render()
        buffer = self.lcd_buffer
        buffer[1] = " name               "
        assert_equal(self.lcd_manager.lcd.buffer, buffer)

Function that pass test:

    def render(self):
        """add widget view to lcd buffer"""
        for widget in self.widgets:
            position = widget.position
            self.lcd.write(
                widget.render()[0],
                position['x'],
                position['y']
            )

See that we have used naive fetching 0 indexed row from array and pass it to buffer. But this allow us to pass current test. With more advanced widgets and tests we will change it.
Now we need to be sure that calling flush on Manager calls flush on lcd.

    from mock import MagicMock

    def test_flush_should_call_lcd_flush(self):
        self.lcd_manager.lcd.flush = MagicMock()
        self.lcd_manager.flush()
        self.lcd_manager.lcd.flush.assert_called_once_with()

Run tests and we are red, but simple function:

    def flush(self):
        """display content"""
        self.lcd.flush()

Take us back to green.

Label demo – second step

Now we have all bricks to build something. Go back to demo and:


#!/usr/bin/python
# -*- coding: utf-8 -*-

"""LCD Manager - label demo"""


import sys
sys.path.append("../../")
import RPi.GPIO as GPIO #pylint: disable=I0011,F0401
from charlcd import buffered as buffered
from charlcd.drivers.gpio import Gpio
from charlcd.drivers.i2c import I2C
from lcdmanager import manager
from lcdmanager.widget.label import Label

GPIO.setmode(GPIO.BCM)


def label1():
    lcd = buffered.CharLCD(16, 2, I2C(0x20, 1))
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    label1 = Label(1, 1)
    label1.label = "Hello !"
    lcd_manager.add_widget(label1)
    lcd_manager.render()
    lcd_manager.flush()

label1()

Run it and success!

But see what happen when you set label at position (0, 0)… it wont allow us. Its definitely a bug.

So add new test 🙂 :

    def test_set_position_0_0(self):
        self.widget.position = {'x': 0, 'y': 0}

And kaboom ! Quick fix (in widget):

    @position.setter
    def position(self, pos):
        """sets widget position"""
        if not isinstance(pos, dict):
            raise AttributeError("dict with x and y required")
        if not 'x' in pos or not 'y' in pos:
            raise AttributeError("x and y required")
        self._position = pos

Stop! Jenkins time!

I forgot to define _position and _visibility in widget.
All __init__.py files were incorrect.
I had to rewrite label demo.

Summary

We have build lcd manager skeleton, created abstract widget class and created first widget: label.

In this article I also showed how TDD methodology can be used.

Download code.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s