Learning how to properly structure a Python repository on GitHub, with unit tests, and Travis integration.
- Ideas stolen from The Hitchhiker's Guide to Python.
- Structure of the repository
- Python testing tools
- pytest
- Continuous Integration
Keep it simple. Have a readme.md.
Python modules are just valid python files. When you "import" a module, the file is executed by the python iterpreter. Read the docs for more.
A Python package is just a directory structure containing modules. You add a __init__.py file to indicate that a directory is a package. By default, this does nothing, and so it is common to include further import commands.
- For example,
import pointsruns two commands:import points.nnwhich means thatpoints.nnis imported into your namespacefrom points.point import Pointwhich means thatPointnow becomes a type in your namespace- see
example.pyfor usage. An interesting exercise is to change__init__.pyto be empty, and see how thenexample.pyneeds to be modified.
To learn more, browse around github and see how they structure imports. Or look at packages in your local python install.
Here I use pytest. The docs are good. I haven't explored, much at all, the features pytest gives you. The Python standard library comes with the unittest module which is more object orientated. The mocking abilities in unittest.mock are very useful however.
I have put tests in the tests directory:
- Read and re-read Conventions for Python test discovery
- Decide that we should put an empty
__init__.pyfile intests. This will make pytest recurse back to the main directory before running tests. You want to be here to import the package. - An alternative is to put tests in the same directory as the module they test. Coming from a Java/Maven world, this seems cluttered to me.
You can run the tests by executing pytest or py.test in the root directory of the project.
See tests readme for more.
The Travis documentation is very clear.
- Read Getting started. Skip over the language list and follow the instructions to link GitHub to Travis.
- Now read the Python section.
If it all works, you should see your project on the travis page: here's mine. To finish, we should of course:
What's going on under the hood is that Travis will build your project on a set virtual machine image "in the cloud" (aka on one of their servers). For python, there is no compile stage, and so the "build" stage is actually to run all of the tests.
So now we have unit tests, and we're running them on every push thanks to Travis. How do we know that we're testing everything we should? For this, we need a code coverage tool, and the common one to use with GitHub is:
- Codecov.io Visit this and sign-up with your GitHub account
- Use this example: Codecov Python Example and follow the examples.
- I ended up adjusting
requirements.txtand.travis.yml. - See the source for this
readme.mdfile for how to add the codecov badge.
We should list the packages we require, both to help users, and to allow Travis to correctly install the dependencies.
- As an example, fork this repository, and change
requirements.txtto an empty file - The build should then fail, as
scipywill no longer be found. - Travis installs
numpyautomatically, along withpytest, but little else.
Lots more documentation here.
Things were going so nicely... but making nice documentation seems hard.
This at least is easy.
- TODO: Lots more to say here
To produce, say, HTML documentation which includes discussion, and extracted docstrings, the current best practice seems to be to use Sphinx.
- Hitchhiker's guide to documentation which sadly seems to make it seem easier than it is.
- Sam Nicholls guide which is a more realistic guide to the pain which this is.
- Guide to reStructuredText
Clearly I need to spend some more time playing with Sphinx. To get it at least vaguely working, I needed to slightly adapt Sam Nicholls's comment. You need that when conf.py is run, from whatever directory it has ended up in, that the source code for our project can be imported. As laid out here, we have docs\sources\conf.py while the package points can be imported from the root. So relative to where conf.py is we need to step back two directories. To do this, I added/edited the following in conf.py:
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join('..','..')))
This makes the python interpreter search two directories up for imports. Also do follow Sam's suggestion as regards running sphinx-apidoc (the generated files are now in source control, but by default, after running sphinx-quickstart they are not).
The end result is not yet pretty, but at least works.
We have no intention of doing this for our example, but the standard pip supported way is to visit pypi.python.org and follow the instructions.
- Peter Downs' guide seems very nice
- Packaging user guide the official docs
- Guide to
distutilsfrom the Python docs
Once you have a correctly written setup.py file, you can run python setup.py install which will automatically install the module points. You can now run python anywhere and import points will work, reading the installed version, instead of the development version. At least on my Windows install of Anaconda, you can see where the files are installed, and to "uninstall" simply delete them.
If you have uploaded your package to PyPI correctly, then you can run pip (un)install <package-name>.