Writing and Distributing Open Source Python Software

Overview

This tutorial will walk through the steps to make a distributable python package, host the package on github, distribute the package on PyPi, the Python Package Index, write documentation for your package, and host the docs on readthedocs. The instructions are for Ubuntu/Linux 64-bit operating systems. This is a bare bones introduction which should provide you with the essential info for getting started distributing software. The online literature for each aspect of distributing python software can be pretty daunting. I've included links to the clearest and most concise treatments I've found on the separate topics.

Setting up accounts

To complete this tutorial you will need to set up (free) accounts for Github, PyPi, and ReadTheDocs linked to a common email address if you don't already have these accounts set up. You can find instructions at the links below:

Markup languages

reStructuredText and Markdown are lightweight markup languages used for documentation among other things. This tutorial is written in Markdown. The docs for the toy python package used for demonstration are written in rSt.

To find out more about these common markup languages see the links below:

Python Version Compatibility

The world of python users is currently split between python2 and python3, which have some compatibility issues. If you want your code accessible to the greatest number of people you should try to write python code that is backwards/forwards compatible. Here is a link to some tools to help with the compatibility issue.

http://python-future.org/automatic_conversion.html

Pip and Virtualenv

Pip is the tool that is used by PyPi to distribute python software. Sometimes it is good to install new python software in a virtual environment to make sure that it doesn't conflict with the core python distribution of your OS. To install pip and virtualenv:

$ sudo apt-get install python-pip python-virtualenv

Git

You are going to need git installed on your machine. git is version control software used by github which acts as a central repository for your software. You can learn more about git here https://blog.udemy.com/git-tutorial-a-comprehensive-guide/. To install git:

$ sudo apt-get update
$ sudo apt-get install git

Python Packages and Modules

A python module is simply a .py file with reusable python functions and classes. A python package is folder that contains a collection of python modules and packages that has a special __init__.py file at the top level. This file can simply be blank which is considered good practice, but can also be used to share code between the package's subpackages and modules.

For an in depth explanation of python packages and modules see here http://www.learnpython.org/en/Modules_and_Packages

helloMyName

I made a toy software package which is just complicated enough to explain the basics of package distribution, but simple enough to still be able to understand what is going on. You should clone this package from github. From the command line, in the directory where you want the package:

$ git clone https://github.com/aarontuor/helloMyName.git

I made this package following the minimal structure tutorial found here http://python-packaging.readthedocs.io/en/latest/minimal.html

I've already set up helloMyName on github, pypi, and readthedocs. Let's set up a virtual environment to install helloMyName and run it's limited utility in a python terminal.

$ mkdir ~/pythonDist
$ virtualenv --system-site-packages ~/pythonDist
$ source ~/pythonDist/bin/activate
$ pip install helloMyName
$ python
> from helloMyName import welcome
> welcome.hi(8, mood='depressed', verbose=True)
hello world! I am depressed to be a program.
I am veeeeery depressed to meet you
8

It doesn't do much but it is freely distributed software!

helloYourName

Now let's use the code you have cloned to make your own distribution! First let's get rid of everything that was specific to my distribution:

  1. First let's change the helloMyName folders by replacing 'MyName' with your name or whatever you like. From now on in this document I'll refer to whatever you replaced 'MyName' with as 'YourName'. So where ever you see the string of characters 'YourName' you should replace with whatever you replaced the characters 'MyName' with.

  2. Now in the top level helloYourName folder:

$ rm -rf docs
$ rm -rf dist
$ rm -rf .git

Structure

helloYourName has the following structure:

helloYourName # top level of distribution
    setup.py # for installing the package
    README.rst # for package's github homepage
    helloYourName # package
        __init__.py # so python knows this is a package
        welcome.py # the module of reusable python functions

So, our distributed code contains one package and one module.

Now let's look at the contents of setup.py, the code that is used to install your program.

setup.py

from setuptools import setup, find_packages

setup(name='helloMyname',
      version=0.02,
      description='Python package example',
      url='http://mywebsite',
      author='myname',
      author_email='tuora@students.wwu.edu',
      license='none',
      packages=find_packages(), # or list of package paths from this directory
      zip_safe=False,
      install_requires=[],
      classifiers=['Programming Language :: Python'],
      keywords=['Package Distribution'])

You'll wan't to change the name, url, author, and author_email arguments for your package. Also you probably want an open source license named LICENSE, in the top level directory of helloYourName. A guide to open source licenses can be found here http://docs.python-guide.org/en/latest/writing/license/

You can install this package manually by running the setup.py script with either a 'develop' or 'install' argument. Assuming you are in the virtual environment we set up and in the top level directory of helloYourName if you run:

$ python setup.py develop

Then a symlink to the source code will be copied to the directory ~/pythonDist/lib/python2.7/site-packages/helloYourName. If you use 'install' instead of 'develop' all the source in helloYourName will be copied to this location instead.

For a more detailed introduction to basic info about setup.py files look here: http://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/quickstart.html.

Documenting with Sphinx

We will automatically generate most of our documentation from python doc-strings using Sphinx, the python documentation generator. So we need to install Sphinx. I recommend a virtual environment for installing Sphinx but it isn't strictly necessary. In the top level directory of helloYourName:

$ pip install --upgrade sphinx sphinx-autobuild # installs Sphinx
$ mkdir docs 
$ cd docs
$ sphinx-quickstart # populates docs directory with basic sphinx docs build

At this point you will be asked a series of questions to customize your initial docs build. Many of these you can just hit enter and choose the default. Some information you must provide, such as the project name (helloYourName), the author name (YourName), the project version (0.1), and the project release (0.1.1). Version numbers typically have two numbers separated by a decimal and release numbers have the version number and some release qualifier after another decimal. There are many lengthy discussions online about version and release numbers. The most straightforward can be found here http://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/specification.html. Basically each new version and release should have a larger number. So, start small and work your way up.

Here are some questions which you should answer yes to so that important extensions are enabled:

autodoc: automatically insert docstrings from modules (y/n) [n]: y 
doctest: automatically test code snippets in doctest blocks (y/n) [n]: y 
mathjax: include math, rendered in the browser by MathJax (y/n) [n]: y
viewcode: include links to the source code of documented Python objects (y/n) [n]: y

Now you can make your first docs:

$ make html # makes html files in docs/_build/html/

Now the framework of your documentation is set up! You can view these docs at helloYourName/docs/_build/html/index.html.

The sphinx documentation is pretty dense and bewildering but you can find it here http://www.sphinx-doc.org/en/stable/.

Customization and conf.py

sphinx-quickstart populated your docs directory with a file conf.py. This is the file that tells sphinx where you specify all the configurable options.

Find a small image (your_pic.png) to use as your logo, and place it in helloYourName/docs/_static. Add these lines in the appropriate spots in conf.py.

import os
import sys
sys.path.append(os.path.abspath('sphinxext')) # lets python know where to find your sphinx extensions
sys.path.append(os.path.abspath('../helloYourName/')) # This is so sphinx knows where to find your module
html_theme = "sphinx_rtd_theme" # Nice clean theme.
html_logo = '_static/your_pic.png' # adds logo to documents pages.

index.rst

The index.rst file that sphinx-quickstart populated in your docs folder is analogous to an index.html file for a website. It is the base page of your documentation and may be all you really need for a small package. We'll go into adding pages a little later. Your index.rst file should look something like below:

.. helloYourName documentation master file, created by
   sphinx-quickstart on Wed Jan  4 16:40:47 2017.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

Welcome to helloYourName's documentation!
=======================================

Contents:

.. toctree::
   :maxdepth: 2



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Docstrings and Autodoc

Add these lines above 'Indices and tables' in index.rst so that your docstrings in the welcome module will be used to auto-document the module.

.. automodule:: welcome
   :members:
   :undoc-members:

Let's look at how the docstring is formatted to achieve this magic!

def hi(some_number, mood='excited', verbose=False):
    """
    Fancy hello world function. :math:`\sum_{i=1}^{10} badDocs*BadlyWrittenCode = frustration`

    :param some_number: (int) Something to return.
    :param mood: (str) A string describing a mood.
    :param verbose: (boolean) Where or not to be a gushing hello world program.
    :return: (int) some_number
    """

These are rSt style docstrings. Other popular styles of python docstrings are numpy, and google style docstrings which each follow their own syntax. A concise description of these different formats of docstrings can be found here: http://daouzli.com/blog/docstring.html. In order to parse numpy and google docstrings with sphinx autodoc you need to add the sphinx extension Napolean http://www.sphinx-doc.org/en/1.5.1/ext/napoleon.html. The pycharm IDE will auto-complete your docstrings for you in any of these styles if you type three quotation marks and return.

Doctest

It's nice to give examples in your documentation, but you should make sure that your examples give the correct output. Doctest was designed for this:

In your docstring for the hi function include the following:

:examples:

    >>> from helloAaron import welcome
    >>> welcome.hi(1, 'thrilled', verbose=True)
    hello world! I am thrilled to be a program.
    I am veeeeery thrilled to meet you
    1

Now from docs run:

$ make html
$ make doctest

Distributing the code on pypi

From the helloYourName directory:

$ python setup.py register 
$ python setup.py sdist upload

You should be prompted for your PyPi user name and password. If for some reason the software gets confused you can add a .pypirc file with the following contents:

[server-login]
repository=https://pypi.python.org/pypi
username:yourPyPiUserName
password:yourPyPiPassword

Now your software is hosted on PyPi the Python Package Index!

Now anyone can install your package by:

$ pip install helloMyName --user

Try installing your neighbor's package and trying out the code:

In [1]:
from helloYourName import welcome
In [2]:
welcome.hi(1, 'sad', True)
hello world! I am sad to be a program.
I am veeeeery sad to meet you
Out[2]:
1

hosting the code on github

  1. Make a repo on github called helloYourName.
  2. Link the repo as the remote of your local helloYourName top level directory:
$ git init
$ git add --all
$ git commit -m 'first commit'
$ git remote add origin https://github.com/<YourGitHubUserName>/helloYourName.git
$ git push origin master

Hosting docs on read the docs

First let's set it up so that your github will sync with your readthedocs account.

If your project is hosted on GitHub, you can easily add a hook that will rebuild your docs whenever you push updates. From your repo on github:

  • Go to the "Settings" page for your project
  • Click "Webhooks & Services"
  • In the "Services" section, click "Add service"
  • In the list of available services, click "ReadTheDocs"
  • Check "Active"
  • Click "Add service"

Note

The GitHub URL in your Read the Docs project must match the URL on GitHub. The URL is case-sensitive.

If you ever need to manually set the webhook on GitHub, you can point it at https://readthedocs.org/github.

  1. Go to readthedocs and sign in.
  2. Click on Import a Project
  3. If your project pops up on the list of github repos double click on it.
  4. If your project doesn't pop up on the list of github repos click on Import Manually
  5. Enter Name: helloYourName
  6. Repository Url: https://github.com/your_github_username/helloYourName.git
  7. Click Next.
  8. Click Build. Wait a bit.
  9. Click View Docs.

Congratulations your docs are hosted on readthedocs!

Command line scripts

You may include some command line python scripts in your package. Let's make the following script at helloMyName/bin/helloWorld.py:

#!/usr/bin/env python

import argparse

def return_parser():
    parser = argparse.ArgumentParser("Command line tool to print Hello World and demonstrate documenting scripts")
    parser.add_argument("arg0", type=int, help="A needless integer argument for demonstration")
    parser.add_argument("-needless_arg_1", type=int, default=1, help="A needless integer argument for demonstration")
    parser.add_argument("-needless_arg_2", type=str, default='oops', help="A needless string argument for demonstration")
    return parser

if __name__ == '__main__':
    args = return_parser().parse_args()
    print(args.arg0)
    print(args.needless_arg_1)
    print(args.needless_arg_2)
    print('Hello world!')

Now in helloMyName/setup.py add the following argument to the call to setup:

scripts=['bin/helloWorld']

We can test out this command line script now. From the top level helloMyName directory:

$ python setup.py develop
$ helloWorld 1
1
1
oops
Hello world!

To document the script, in helloMyName/docs/index.rst include the following line:

helloWorld.py
==================
.. argparse::
   :ref: helloWorld.return_parser
   :prog: helloWorld

Now in conf.py include 'sphinxarg.ext' in the extensions list.

Now from the command line:

$ pip install sphinx-argparse

Now from the helloMyName/docs/ directory:

make html

Additional Documentation pages

You can also link additional pages to your documentation. In helloMyName/docs/index.rst in the toctree section include these lines:

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   welcome.rst
   goodbye.rst

Now make your welcome.rst, and goodbye.rst files in the docs directory.

welcome.rst

welcome
=======

This is a tutorial of how to use the welcome.rst module. It's really great.

### goodbye.rst

goodbye

We plan to extend this package to include a goodbye module. ```

Now remake the docs with the make html command.

README.rst

For a few final touches let's customize your README.rst file. Right now the README.rst has all my info in it so you should swap out the urls and names for your docs and project name. Don't forget to add, commit and push your changes to github!

If you don't like using rSt for your README github will also parse a README.md file.

Updating package on Pypi and ReadTheDocs

Now that we've made some changes let's update the project on Pypi and ReadTheDocs.

  1. Change the version number in setup.py and conf.py.
  2. add, commit, and push changes to github
  3. Upload project to pypi.
  4. Make new docs on readTheDocs.
  5. Whoops error! Fetch argparse ext from pypi, unpack in sphinxext folder and copy sphinxarg folder up one dir.
In [ ]: