github gitlab twitter
Extending our Cookiecutter template
Jan 18, 2015
5 minutes read

In the previous part of this tutorial series, we learned how to create a Cookiecutter template for a basic Kivy app. Next we are going to add tests and documentation. We will also create setup.py and Makefile files.

The template it its current state contains the following directories and files:

$ tree cookiedozer
cookiedozer/
├── cookiecutter.json
├── {{cookiecutter.repo_name}}
│   ├── {{cookiecutter.repo_name}}
│   │   ├── {{cookiecutter.app_class_name}}.kv
│   │   ├── {{cookiecutter.repo_name}}.py
│   │   ├── __init__.py
│   │   └── main.py
│   ├── LICENSE
│   └── README.rst
├── cookiedozer01.png
├── cookiedozer02.png
├── hooks
│   └── post_gen_project.py
├── LICENSE
└── README.rst

Tests

We’re going to use pytest for the tests. We follow the recommendations for organizing tests and keep the tests separated from our application code. Doing so, allows us to easily exclude them from the distribution later on.

pytest is a mature testing framework for Python that is developed by a thriving and ever-growing community of volunteers. It uses plain assert statements and regular Python comparisons.

$ cd cookiedozer
$ mkdir \{\{cookiecutter.repo_name\}\}/tests

Now let’s create a test file:

$ cd \{\{cookiecutter.repo_name\}\}/tests
$ touch test_{{cookiecutter.repo_name}}.py

We can use Kivy’s Interactive Launcher to run a non-blocking app.

# -*- coding: utf-8 -*-

import pytest


@pytest.fixture(scope="module")
def app(request):
    """Uses the InteractiveLauncher to provide access to an app instance.

    The finalizer stops the launcher once the tests are finished.

    Returns:
      :class:`{{cookiecutter.app_class_name}}`: App instance
    """
    from kivy.interactive import InteractiveLauncher
    from {{cookiecutter.repo_name}}.{{cookiecutter.repo_name}} import {{cookiecutter.app_class_name}}
    launcher = InteractiveLauncher({{cookiecutter.app_class_name}}())

    def stop_launcher():
        launcher.safeOut()
        launcher.stop()

    request.addfinalizer(stop_launcher)

    launcher.run()
    launcher.safeIn()
    return launcher.app


def test_app_title(app):
    """Test that the default app title meets the expectations.

    Args:
      app (:class:`{{cookiecutter.app_class_name}}`): Default app instance

    Raises:
      AssertionError: If the title does not match
    """
    assert app.title == '{{cookiecutter.app_title}}'


@pytest.fixture(scope="module")
def carousel(app):
    """Fixture to get the carousel widget of the test app."""
    return app.carousel


def test_carousel(carousel):
    """Test for the carousel widget of the app checking the slides' names and
    the text of one of the slide labels.

    Args:
      carousel (:class:`Carousel`): Carousel widget
        of :class:`{{cookiecutter.app_class_name}}`

    Raises:
      AssertionError: If the first slide does not contain *Hello*
      AssertionError: If the names of the slides do not match the expectations
    """
    assert '{{cookiecutter.app_title}}' in carousel.current_slide.text

    names = [slide.name for slide in carousel.slides]
    expected = ['hello', 'kivy', 'cookiecutterdozer', 'license', 'github']
    assert names == expected

I am using a special syntax for sphinx doc strings, more about this later.

Following the advice of the aforementioned pytest docs, I do not create an __init__.py inside the tests directory. To run the the suite later on we need to install our package first. Otherwise we are not able to import from any of the application modules.

Create an installable package

First we need to create a setup.py file at the top level of our package (not the template root):

$ cd ..
$ touch setup.py

We will use setuptools to configure our distribution package and specify a number of classifiers. We will also implement a custom CLI command to run our tests via python setup.py test, as described in the pytest docs.

import sys
import os

from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand


def read(fname):
    return open(os.path.join(os.path.dirname(__file__), fname)).read()


class PyTest(TestCommand):
    user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]

    def initialize_options(self):
        TestCommand.initialize_options(self)
        self.pytest_args = []

    def finalize_options(self):
        TestCommand.finalize_options(self)
        self.test_args = []
        self.test_suite = True

    def run_tests(self):
        import pytest
        errno = pytest.main(self.pytest_args)
        sys.exit(errno)


setup(
    name='{{cookiecutter.repo_name}}',
    version='{{cookiecutter.version}}',
    author='{{cookiecutter.full_name}}',
    author_email='{{cookiecutter.email}}',
    description='{{cookiecutter.short_description}}',
    long_description=read('README.rst'),
    license='MIT',
    keywords=(
        "Python, cookiecutter, kivy, buildozer, pytest, projects, project "
        "templates, example, documentation, tutorial, setup.py, package, "
        "android, touch, mobile, NUI"
    ),
    url='https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.repo_name}}',
    install_requires=['kivy>=1.8.0'],
    zip_safe=False,
    packages=find_packages(),
    package_data={
        '{{cookiecutter.repo_name}}': ['*.kv*']
    },
    entry_points={
        'console_scripts': [
            '{{cookiecutter.repo_name}}={{cookiecutter.repo_name}}.main:main'
        ]
    },
    tests_require=['pytest'],
    cmdclass={'test': PyTest},
    classifiers=[
        'Development Status :: 2 - Pre-Alpha',
        'Environment :: X11 Applications',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: MIT License',
        'Natural Language :: English',
        'Operating System :: POSIX :: Linux',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.7',
        'Topic :: Artistic Software',
        'Topic :: Multimedia :: Graphics :: Presentation',
        'Topic :: Software Development :: User Interfaces',
    ],
)

To include non-Python files, such as README.rst and LICENSE, we need to create file with name MANIFEST.in with the following content:

include LICENSE
include README.rst

Virtual Environment

It is recommended to use virtual environments to isolate project dependencies from your system Python.

To make this very convenient, I highly recommend virtualenvwrapper to manage your virtual environments. Given that you successfully installed the wrapper you can run the following set of commands:

$ cookiecutter cookiedozer/
$ cd helloworld
$ mkvirtualenv helloworld
$ setvirtualenvproject
$ toggleglobalsitepackages

We generate a new project using the default repository name (helloworld), create and activate a new virtual environment with the same name and connect the current working directory with the env. Finally I enable the global site packages in the virtual environment as I installed kivy to my systems’ python.


Install the app

Using develop setup command over install, allows us to change our application code without having to reinstall the package.

$ python setup.py develop

Run the app

With the package installed, we can now run our app:

$ helloworld

Run the tests

Running the test suite is just as easy.

============================ test session starts ============================
platform linux2 -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
collected 2 items

tests/test_helloworld.py ..

========================= 2 passed in 0.68 seconds ==========================

In the final part of this tutorial we are going to set up a sphinx based documentation and also create a buildozer specification that helps us deploy the app to an Android mobile device.

We are going to wrap up this tutorial by writing a Makefile to implement a few commands for the most important tasks. 😎


Back to posts