Integrating GitHub with PyPi, Travis-CI, ReadTheDocs, and Code Climate

Integrating GitHub with PyPi, Travis-CI, ReadTheDocs, and Code Climate

After change is pushed into GitHub, Travis is used to build and deploy PyPi package. Generated documentation is readable in PyPi, GitHub, and Read The Docs.

Results

Final results looks like this:

How To Get There

Register and create new repository on GitHub. Register on PyPi and Read The Docs (RTFD). Sign up with GitHub account for Travis-CI and Code Climate.

Selecting Markup Syntax

PyPi requires reStructuredText, RTFD depends on Sphinx which is also using reStructuredText as markup. GitHub also supports reStructuredText, so it’s best to use reStructuredText.

Creating Python Package

First step is to create a python package. Detailed documentation is available here, but in practice it’s sufficient to use some existing setup.py, requirements.txt, and README.rst and modify them. For list of available classifiers for setup.py check following list. LICENSE file you will get for free when you create new repository on GitHub.

Generating Documentation

Now, when you have your initial README.rst file, it’s time to set up documentation generation. Sphinx tutorial describes whole process in details. You need to run: pip install Sphinx and sphinx-quickstart in your package directory. When you answer some questions, you will get conf.py which contains instructions for sphinx/RTFD and Makefile for generating documentation.

You can generate you documentation with make html and see it with chromium-browser _build/html/README.html. Although, it will look completely fine, it will be reported as failed build in RTFD, if you don’t include:

.. toctree::
   :maxdepth: 2

However, if you try to generate documentation in the same way, as it will be generated by PyPi with

python3 setup.py --long-description | rst2html.py > pypi-doc.html

You will get error

<stdin>:11: (ERROR/3) Unknown directive type "toctree".

To avoid this, it’s necessary to preprocess README.rst before it’s used as long description in PyPi documentation. I have achieved that by introducing two special tags – PYPI-BEGIN and PYPI-END which mark section which should be omitted for PyPi. Then setup.py contains following helper function:

def fix_doc(txt):
	return re.sub(r'\.\. PYPI-BEGIN([\r\n]|.)*?PYPI-END', '', txt, re.DOTALL)

with open('README.rst') as fileR:
    README = fix_doc(fileR.read())

...
  long_description=README,

Now, your documentation is updated on RTFD, it’s readable on GitHub and it will also look good on PyPi.
Version, that you have specified in setup.py should match version specified in conf.py.

Deploying to PyPi

It’s great to have automatically generated documentation, but without package itself it’s useless. So, lets create PyPi package. To do that, you can use Travis-CI. They have documentation how to use travis client as well as how to set up travis config to deploy packages on PyPi.

First, you have to install travis client with gem install travis and then you have to create .travis.yml file. You can also use following template.

language: python
python:
- '2.7'
- '3.3'
- '3.4'
- '3.5'
- '3.6'
install: pip install -r requirements.txt
deploy:
  provider: pypi
  user: user_name
  password:
    secure: somelongstring
  on:
    tags: true
    branch: master

To make it work, you have to do few more steps. Login to GitHub with travis client with travis login. You have to use your GitHub credentials in this step. Then you have to change user_name to your user name, that you use on PyPi in .travis.yml. Last step is to run command travis encrypt --add deploy.password and type your password for PyPi. This will modify .travis.yml file. Now it will contain some real value for key secure.

If you password contains some special characters, you should check documentation how to escape them properly.

Based on our configuration, deployment will be executed only when new tag is created, so we have to create one with following commands.

git tag 0.1 -m "Initial commit"
git push --tags origin master

It would be also nice if tag would match version used in setup.py.

Code Climate

Code Climate will analyze your code after each push. Integration with Travis is easy. Generate token and specify it in .travis.yml.

after_sucess:
- CODECLIMATE_REPO_TOKEN=yourtoken codeclimate-test-reporter

Congratulations!!! Now, you have your package available on PyPi.

Deploying New Version

To deploy new version you have to change its number in setup.py and conf.py. After that, you can create new tag and push changes into GitHub repository with

git tag 0.2 -m "New version"
git push --tags origin master

Troubleshooting

Wrong Python Used For Sphinx

I have received this error, when I have on my Ubuntu package python-sphinx which is using Python 2. Solution is to change SPHINXBUILD = python -msphinx to SPHINXBUILD = python3 -msphinx in Makefile.

Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/sphinx/__main__.py", line 14, in 
    sys.exit(main(sys.argv))
  File "/usr/lib/python2.7/dist-packages/sphinx/__init__.py", line 51, in main
    sys.exit(build_main(argv))
  File "/usr/lib/python2.7/dist-packages/sphinx/__init__.py", line 61, in build_main
    from sphinx import cmdline
  File "/usr/lib/python2.7/dist-packages/sphinx/cmdline.py", line 14, in 
    import optparse
  File "/usr/lib/python2.7/optparse.py", line 419, in 
    _builtin_cvt = { "int" : (_parse_int, _("integer")),
  File "/usr/lib/python2.7/gettext.py", line 584, in gettext
    return dgettext(_current_domain, message)
  File "/usr/lib/python2.7/gettext.py", line 548, in dgettext
    codeset=_localecodesets.get(domain))
  File "/usr/lib/python2.7/gettext.py", line 483, in translation
    mofiles = find(domain, localedir, languages, all=1)
  File "/usr/lib/python2.7/gettext.py", line 440, in find
    for nelang in _expand_lang(lang):
  File "/usr/lib/python2.7/gettext.py", line 133, in _expand_lang
    from locale import normalize
ImportError: cannot import name normalize
Error in sys.excepthook:
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/apport_python_hook.py", line 63, in apport_excepthook
    from apport.fileutils import likely_packaged, get_recent_crashes
  File "/usr/lib/python2.7/dist-packages/apport/__init__.py", line 5, in 
    from apport.report import Report
  File "/usr/lib/python2.7/dist-packages/apport/report.py", line 12, in 
    import subprocess, tempfile, os.path, re, pwd, grp, os, time
  File "/usr/lib/python2.7/tempfile.py", line 32, in 
    import io as _io
  File "/usr/lib/python2.7/dist-packages/sphinx/io.py", line 11, in 
    from docutils.io import FileInput
  File "/usr/lib/python2.7/dist-packages/docutils/io.py", line 18, in 
    from docutils.utils.error_reporting import locale_encoding, ErrorString, ErrorOutput
  File "/usr/lib/python2.7/dist-packages/docutils/utils/__init__.py", line 21, in 
    from docutils.utils.error_reporting import ErrorOutput, SafeString
  File "/usr/lib/python2.7/dist-packages/docutils/utils/error_reporting.py", line 47, in 
    locale_encoding = locale.getlocale()[1] or locale.getdefaultlocale()[1]
AttributeError: 'module' object has no attribute 'getlocale'

Original exception was:
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/sphinx/__main__.py", line 14, in 
    sys.exit(main(sys.argv))
  File "/usr/lib/python2.7/dist-packages/sphinx/__init__.py", line 51, in main
    sys.exit(build_main(argv))
  File "/usr/lib/python2.7/dist-packages/sphinx/__init__.py", line 61, in build_main
    from sphinx import cmdline
  File "/usr/lib/python2.7/dist-packages/sphinx/cmdline.py", line 14, in 
    import optparse
  File "/usr/lib/python2.7/optparse.py", line 419, in 
    _builtin_cvt = { "int" : (_parse_int, _("integer")),
  File "/usr/lib/python2.7/gettext.py", line 584, in gettext
    return dgettext(_current_domain, message)
  File "/usr/lib/python2.7/gettext.py", line 548, in dgettext
    codeset=_localecodesets.get(domain))
  File "/usr/lib/python2.7/gettext.py", line 483, in translation
    mofiles = find(domain, localedir, languages, all=1)
  File "/usr/lib/python2.7/gettext.py", line 440, in find
    for nelang in _expand_lang(lang):
  File "/usr/lib/python2.7/gettext.py", line 133, in _expand_lang
    from locale import normalize
ImportError: cannot import name normalize
Makefile:23: recipe for target 'html' failed
make: *** [html] Error 1

Travis Permissions

If you are receiving error message user-name has not granted Travis CI the required permissions, please log in via travis-ci.com, it’s caused by using travis login --pro instead of travis login. You should modify all commands to not to use --pro flag.

Trackbacks/Pingbacks

Leave a Reply

Your email address will not be published. Required fields are marked *

ˆ Back To Top