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. 😎