Libraries and Modules in Python

Making a Package

Overview:

  • Teaching: 10 min
  • Exercises: 5 min

Questions

  • How do modules in Python work?
  • What is a package?
  • How do I install, structure and distribute a package?

Objectives

  • Learn about Python module and package structure
  • Use pip to install a local package
  • Learn how to structure a package and write setup.py

We met modules in the previous episode. They were useful as we could put functions and classes into a seperate file and access them from other Python scripts. This was done by using the import keyword followed by the name of the module. The name of the module is the filename we give it (for single file modules at least).

If we make our own module, it is initially only importable from the current directory, which is fine for a small project, but if you had a module containin routines that you use regularly, it would be good if they could be imported into any of your projects.

Modules

For a better understanding of exactly how modules work tak a look at the Python documentation on modules.

Pip

Python comes with its own package manager called pip. Most commonly it is used to install packages contained in PyPI, the Python Packag Index, but can also be used to install packages stored in git repositories and on the local machine, which is what we will look at here.

We can demonstrate how to use pip by reinstalling the superheros module, if you followed through the setup episode this will be installed already, but pip will know this and reinstall anyway. We can install (or reinstall) the local package in ./code/superheros by running

In [1]:
!pip install ./code/superheros
Processing /home/travis/build/arc-lessons/libraries-modules/code/superheros
Building wheels for collected packages: superheros
  Building wheel for superheros (setup.py) ... - done
  Stored in directory: /tmp/pip-ephem-wheel-cache-_cr1b9vq/wheels/93/1c/58/307c11b37daa5ca7fb8c87ed43a397c24fec99ec9a83f41f51
Successfully built superheros
Installing collected packages: superheros
  Found existing installation: superheros 1.0
    Uninstalling superheros-1.0:
      Successfully uninstalled superheros-1.0
Successfully installed superheros-1.0

It's as simple as that! pip is the package manager, install is the instruction we want pip to carry out, and ./code/superheros is the location of our package. If we instead wanted to install a package from PyPI we just put the name of the package in place of the path, for instance

In [2]:
!pip install nbfancy
Requirement already satisfied: nbfancy in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (0.1.dev2)
Requirement already satisfied: jupyter in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbfancy) (1.0.0)
Requirement already satisfied: ipython>=6 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbfancy) (7.5.0)
Requirement already satisfied: nbconvert in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter->nbfancy) (5.5.0)
Requirement already satisfied: qtconsole in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter->nbfancy) (4.5.1)
Requirement already satisfied: ipykernel in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter->nbfancy) (5.1.1)
Requirement already satisfied: ipywidgets in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter->nbfancy) (7.4.2)
Requirement already satisfied: jupyter-console in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter->nbfancy) (6.0.0)
Requirement already satisfied: notebook in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter->nbfancy) (5.7.8)
Requirement already satisfied: pickleshare in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (0.7.5)
Requirement already satisfied: traitlets>=4.2 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (4.3.2)
Requirement already satisfied: backcall in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (0.1.0)
Requirement already satisfied: decorator in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (4.4.0)
Requirement already satisfied: pygments in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (2.4.2)
Requirement already satisfied: prompt-toolkit<2.1.0,>=2.0.0 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (2.0.9)
Requirement already satisfied: setuptools>=18.5 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (40.8.0)
Requirement already satisfied: pexpect; sys_platform != "win32" in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (4.7.0)
Requirement already satisfied: jedi>=0.10 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipython>=6->nbfancy) (0.13.3)
Requirement already satisfied: mistune>=0.8.1 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (0.8.4)
Requirement already satisfied: pandocfilters>=1.4.1 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (1.4.2)
Requirement already satisfied: bleach in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (3.1.0)
Requirement already satisfied: testpath in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (0.4.2)
Requirement already satisfied: jinja2>=2.4 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (2.10.1)
Requirement already satisfied: jupyter-core in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (4.4.0)
Requirement already satisfied: defusedxml in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (0.6.0)
Requirement already satisfied: nbformat>=4.4 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (4.4.0)
Requirement already satisfied: entrypoints>=0.2.2 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbconvert->jupyter->nbfancy) (0.3)
Requirement already satisfied: jupyter-client>=4.1 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from qtconsole->jupyter->nbfancy) (5.2.4)
Requirement already satisfied: ipython-genutils in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from qtconsole->jupyter->nbfancy) (0.2.0)
Requirement already satisfied: tornado>=4.2 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipykernel->jupyter->nbfancy) (6.0.2)
Requirement already satisfied: widgetsnbextension~=3.4.0 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from ipywidgets->jupyter->nbfancy) (3.4.2)
Requirement already satisfied: terminado>=0.8.1 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from notebook->jupyter->nbfancy) (0.8.2)
Requirement already satisfied: Send2Trash in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from notebook->jupyter->nbfancy) (1.5.0)
Requirement already satisfied: pyzmq>=17 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from notebook->jupyter->nbfancy) (18.0.1)
Requirement already satisfied: prometheus-client in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from notebook->jupyter->nbfancy) (0.6.0)
Requirement already satisfied: six in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from traitlets>=4.2->ipython>=6->nbfancy) (1.11.0)
Requirement already satisfied: wcwidth in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from prompt-toolkit<2.1.0,>=2.0.0->ipython>=6->nbfancy) (0.1.7)
Requirement already satisfied: ptyprocess>=0.5 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from pexpect; sys_platform != "win32"->ipython>=6->nbfancy) (0.6.0)
Requirement already satisfied: parso>=0.3.0 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jedi>=0.10->ipython>=6->nbfancy) (0.4.0)
Requirement already satisfied: webencodings in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from bleach->nbconvert->jupyter->nbfancy) (0.5.1)
Requirement already satisfied: MarkupSafe>=0.23 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jinja2>=2.4->nbconvert->jupyter->nbfancy) (1.1.1)
Requirement already satisfied: jsonschema!=2.5.0,>=2.4 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from nbformat>=4.4->nbconvert->jupyter->nbfancy) (3.0.1)
Requirement already satisfied: python-dateutil>=2.1 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jupyter-client>=4.1->qtconsole->jupyter->nbfancy) (2.8.0)
Requirement already satisfied: attrs>=17.4.0 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.4->nbconvert->jupyter->nbfancy) (18.2.0)
Requirement already satisfied: pyrsistent>=0.14.0 in /home/travis/virtualenv/python3.7.1/lib/python3.7/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.4->nbconvert->jupyter->nbfancy) (0.15.2)

will install the package nbfancy from PyPI's package repository.

Installing

For further information on different ways we can use pip to install packages see this Python tutorial.

Writing a setup.py

The reason we could pass the directory ./code/superheros to pip and have it install the package, was because it contained a file named setup.py. This Python file contains all the information pip needs to install the modules in superheros to the computer currently being used.

So to make our modules installable, all we have to do is write our own setup.py. After that we can just pass the directory containing setup.py (and our module) to pip and it will attempt to install it.

Packaging and distributing

Clear information about packaging and distributing Python files can be difficult to come by. We have collected the sources of the information we used to make this episode here:

Python tutorials

Python Guide

A guide to both packaging and distributing can be found here, Which goes into more detail than the tutorials. This guide also covers uploading packages to PyPI, the Python Package Index, whic we won't cover here.

Setuptools

Setuptools itself is actually a package and not part of the official core Python, but its use is recommended by both Python and PyPI. Documentation for the project is available on their website.

Setuptools is based on the old distutils module, which was part of core Python. It is largely depricated now and the link is mainly included for interest: distutils

setup.py does not need to contain much information. A minimum working example is included in the file basic_setup.py in the module. (This file won't install the package, as it has the wrong filename!)

The script setup.py is not designed to be executed directly, it is called either by pip or with additional command line arguments to perform tasks. As a result you cannot execute setuptools commands directly into a notebook.

To see what is in the file we can use Python to read in the file and print the content to screen:

In [3]:
with open('./code/superheros/basic_setup.py') as fh:
    print(fh.read())
from setuptools import setup

setup(  name='superheros',
        version=1.0,
        py_modules=['superhero',
                    'superhero2',
                    'check_superheros'
                    ]
        )

We use the package setuptools, as is currently recommended. From the package we import the function setup which uses keyword arguments to provide the information about our package.

  • name is the name of the package that will be used by pip. If we wanted to uninstall this package for some reason, we would pass this name to pip (pip list and pip uninstall commands are uesful here).
  • version is the release version of our package, which is used by pip to update packages, or select specific versions of a package.
  • py_modules is a Python list containing all of the modules we want to include in our package.
  • Normally we would use the packages keyword to specify what to include, but we would need additional file structure for this to work.

That is all the necessary information, however there are far more keywords available, which we can see if we look at the file setup.py that is actually used to install the package:

In [4]:
with open('./code/superheros/setup.py') as fh:
    print(fh.read())
from setuptools import setup

with open("README.md", "r") as fh:
    long_description = fh.read()

setup(  name='superheros',
        version=1.0,
        author='Jack Betteridge',
        author_email='jdb55@bath.ac.uk',
        description='A package about superheros',
        long_description=long_description,
        long_description_content_type='text/markdown',
        url='https://arc-lessons.github.io/libraries-modules/00_schedule.html',
        py_modules=['superhero',
                    'superhero2',
                    'check_superheros'
                    ],
        classifiers=[   'Programming Language :: Python :: 3',
                        'License :: OSI Approved :: BSD License',
                        'Operating System :: POSIX :: Linux'
                    ]
        )

Most of the keywords should be self explanatory, but if in doubt take a look at the documentation linked to above for specific information about any of these keywords.

Package or module

The words package and module (and even library) are often used interchangably when talking about packages and modules. It is only really important in this context. Python defines a module as a single file that contains functions, classes and other code, whereas a Python package is a "module" containing other modules, which in practise means a directory full of Python modules.

For this reason if you want to inculde individual modules the keyword py_module is used. If you go on to write your own package you will probably need the packages keyword.

Using find_packages() in bigger projects

The setuptools package includes the function find_packages() to make creating large packages, with many submodules, more straightforward. This is used in conjunction with the keyword packages to find all of the Python files in a given directory and include them in the package being created, without having to list the files individually.

There is not an equivalent for modules (keyword py_modules) at the top level as this is not a good design for a package. Instead modules should be organised into a structured hierarchy of sub-modules grouped together as appropriate.

Create your own setup.py

Remember morse.py, the module containing the class for encoding and decoding messages as Morse code? It would be great if other people could install this, as it takes at least two people to have a conversation in Morse code!

Create a basic setup.py file for morse.py containing enough information about the module that it can be installed by pip.

Solution

Useful information

Extend your solution to the above exercise by:

  • using some of the additional keywords you have seen in this episode
  • reading the documentation and adding other relevant keywords.

Solution

Key Points:

  • A module is a Python file containing functions and classes.
  • A package is a module that contains other modules.
  • pip is used to install packages, both from PyPI and the local machine.
  • Correct file structure and setup.py are all that are required to install (a distributable) Python package.