Jacob Tomlinson's profile picture Jacob Tomlinson
Home Blog Talks Newsletter About

Publishing open source Python packages on GitHub, PyPI and Conda Forge

10 minute read #python, #github, #tutorial, #packaging, #pypi, #conda-forge, #anaconda

This is part 3 in the Creating an open source Python project from scratch series. If you haven’t read the previous parts you may want to go back and check those out.

In this post we will cover making our code available to people. This is the bit where we open the source! We will push our code to a code posting platform and then package up our library and submit it to a couple of repositories to make it easy for people to install.

Code Hosting

There are many services available for publishing your version controlled project with features such as bug trackers, change review and publishing tools. These platforms will be the place where your community gathers. Users will report bugs and request new features and contributors will submit patches and modifications to the codebase.

As we have used git for our version control a good place to publish this package would be GitHub. However there are alternatives such as BitBucket and GitLab that we could use instead.

To get started you’ll need to sign up for a GitHub Account.

Then you click the “New repository” button in the drop-down menu in the top right.

Create new repo button

Then you need to give your repository a name and a description and click the “Create repository” button.

Create new repo form

Lastly you need to tell your local git repo that you have created a remote repository and that you want to push your code up there.

git remote add origin https://github.com/jacobtomlinson/is-number.git
git push -u origin master

We should also push our version 0.0.1 tag that we created before.

git push origin master --tags

Now if you reload the page we should see our codebase available on GitHub.

GitHub Repository

Note our README is automatically displayed on the page and we can see our commit history in the file explorer.

Releasing/Deployment

Now that our code is available for anyone to see and use let’s look at packaging it up to make it easy for people to install.

Technically folks can already install our package using our published repository on GitHub. A common tool for installing python packages is pip and it supports installing directly from a git repo.

pip install git+https://github.com/jacobtomlinson/is-number.git#egg=is-number

This can be useful if you want to share your project with people for testing or install an unreleased version from the master branch. But to make it accessible to the masses we will want to bundle it up and publish it in a couple of different places.

PyPI

Instead of insisting people install directly from our source repository we can bundle up our code and publish it to the Python Package Index (PyPI). PyPI is the default place for pip to look for packages when installing, you give your package a short name and publish it to PyPI, then folks can install it using that short name.

pip install mypackage

Then when you publish new versions user’s can also use pip to upgrade to the latest version.

pip install --upgrade mypackage

To publish our package we need to register an account.

Then we need to clean up our repository ready for packaging. We can do that by running a command to remove everything in our local folder which has not been tracked with git.

git clean -xfd

Next we need to build our wheel for our package. A wheel is Python’s packaging format. It is basically just a zip file of our source code with the .whl extension and the file name contains some metadata about the package.

python setup.py sdist bdist_wheel

After running this command you will see a load of new files have been generated in your local directory. We don’t actually need to worry about any of these files, but if you’re curious you can check the dist/ directory to see our built wheel.

$ ls dist
is-number-0.0.1+1.gc535984.tar.gz
is_number-0.0.1+1.gc535984-py3-none-any.whl

We can see here we have built the is-number package at version 0.0.1+1.gc535984. This is because after we tagged version 0.0.1 we ran Black on our code and committed the changes. This means the current version of our working directory is 0.0.1 plus 1 commit with the ID gc535984.

We don’t really want to push an unclean version like this to PyPI so we have two options here, we could tag a new version, or check out the old version. As the changes were only code formatting let’s check out the old version and build that instead.

We should clean out these temporary files again first.

git clean -xfd

Now check out version 0.0.1

git checkout 0.0.1

If we run our build command again and list the dist/ directory we should see just version 0.0.1.

$ python setup.py sdist bdist_wheel
$ ls dist
is-number-0.0.1.tar.gz           is_number-0.0.1-py3-none-any.whl

Excellent! Now that we have our versioned package we want to push it to PyPI. Provided your package has a unique name you do not need to create it before hand, you can check by searching your chosen package name on PyPI. To upload our package we are going to use twine which is a utility for uploading to PyPI.

pip install twine

Then use twine to upload our wheels. It will prompt you for your username and password.

$ twine upload dist/*
Enter your username:
Enter your password:
Uploading distributions to https://upload.pypi.org/legacy/
Uploading is_number-0.0.1-py3-none-any.whl
100%|██████████████████████████| 5.85k/5.85k [00:00<00:00, 17.5kB/s]
Uploading is-number-0.0.1.tar.gz
100%|██████████████████████████| 20.1k/20.1k [00:01<00:00, 11.7kB/s]

View at:
https://pypi.org/project/is-number/0.0.1/

We can now visit our package on PyPI.

is-number on PyPI

Our users can now install it with pip, you can try it yourself if you like.

pip install is-number

We should also checkout our master branch again so we can continue working on our project.

git checkout master

Updating our README

The last thing we should do is add this information to our README. We should tell people the command to install the package and also provide a link to the package on PyPI.

It is common place to provide links like this using badges. Many different tools we will come across use badges to communicate information, whether it is package versions, build statuses, etc. An excellent place to get badges for your project is shields.io which is a web service to dynamically generating badge images to use in your README.

If you head to shields.io, search for PyPI and select the then click the PyPI Version badge.

shields.io PyPI version

We can then enter the name of our package and it will show us a preview of our badge and give us an opportunity to copy an RST link to the badge.

shields.io badge preview

Then we should head to our README.rst file and update it with our badge and install instruction.

is-number
=========

.. image:: https://img.shields.io/pypi/v/is-number
   :target: https://pypi.org/project/is-number/
   :alt: PyPI

A Python library to determine if something is a number.

Installation
------------

.. code-block:: bash

   pip install is-number

Then we need to add these changes, commit to git and push to GitHub.

git add README.rst
git commit -m "Add PyPI badge and pip install command"
git push origin master

We can now reload our page on GitHub and see our updated README.

README with PyPI badge and pip install command

Conda Forge

In addition to pip many people also use the conda package manager from Anaconda. There are a few differences between the two package managers but I don’t really want to get into the details of that here, you can have a read of posts like this for yourself. Whether you use pip or conda it is important for us to note that both communities exist and we should make our package available to as many people as possible.

To make our package available via conda we have a couple of options.

The first is that we could create our own channel on Anaconda Cloud and publish our package to it. However that will require users to specify this custom channel when they install your package.

conda install -c mychannel mypackage

This approach is more common when large organisations wish to provide many packages with complex dependencies. You may even wish to repackage dependencies and make them available on your channel.

For open source developers who just want to release a couple of individual packages you can publish your package to Conda Forge. Conda Forge is a community of package maintainers with an automated system for publishing to the conda-forge channel on Anaconda Cloud.

To add our package to Conda Forge we need to write a recipe and contribute it to the staged-recipes repository on GitHub. A conda recipe is similar to the setup.py file we created before, but includes a little more information particularly around building and testing the package.

You will need to fork that repository and clone it locally. Then create a new folder under recipes and a new file in there called meta.yaml.

{% set name = "is-number" %}
{% set version = "0.0.1" %}

package:
  name: {{ name|lower }}
  version: {{ version }}

source:
  url: https://github.com/jacobtomlinson/{{ name }}/archive/{{ version }}.tar.gz
  sha256: 890ea2b8aa999154853c7aa862af1d709df696c4c8b4d6e7533904ec514f273b

build:
  noarch: python
  number: 0
  script: "{{ PYTHON }} -m pip install . -vv"

requirements:
  host:
    - python >=3.5
    - pip
  run:
    - python >=3.5

test:
  imports:
    - is_number

about:
  home: https://github.com/jacobtomlinson/is-number
  license: MIT
  license_family: MIT
  license_file: LICENSE
  summary: 'A Python library to determine if something is a number.'
  description: |
    A Python library to determine if something is a number.    
  doc_url: https://github.com/jacobtomlinson/is-number
  dev_url: https://github.com/jacobtomlinson/is-number

extra:
  recipe-maintainers:
    - jacobtomlinson

We start by specifying the name of the package. Then we have to tell Conda Forge where to find the source for it. As we already have our package on PyPI we can point to that and use the Jinja2 templating to automatically fill in the URL for us. We should also specify the sha256 hash for the file to ensure nobody tampers with our recipe. We can calculate this by downloading the package ourselves, checking the contents and then generating the hash.

wget https://github.com/jacobtomlinson/is-number/archive/0.0.1.tar.gz
tar xvzf 0.0.1.tar.gz
cat is-number-0.0.1/is_number/__init__.py  # Check this is what you expect

If you are happy then generate the hash and update the recipe.

$ openssl sha256 0.0.1.tar.gz
SHA256(0.0.1.tar.gz)= 890ea2b8aa999154853c7aa862af1d709df696c4c8b4d6e7533904ec514f273b

Next we specify how to build our package. As we are just making a simple Python package we can just use pip to install it. We also need to ensure our build number starts at 0.

Then we specify our requirements. For this example we just need Python and pip, but if we had put something into our requirements.txt file we would also need to specify that here. This is one downside of Conda Forge, if your dependency is not available on Conda Forge you may need to add the recipe for it yourself and take on the maintenance burden of keeping it up to date.

Next we specified a test, for this package we just check that we can import it.

We then add some metadata about the package, this is mostly copied from the setup.py file.

Lastly we add a list of maintainers of the recipe. This is because once this recipe has been accepted Conda Forge will automatically create a “feedstock” which is a separate repository on GitHub which contains this recipe and some automations for it. It will also automatically detect new releases on PyPI and make a Pull Request against the feedstock with an updated version and hash. It is up to you as the recipe maintainer to review and merge this Pull Request as there may need to be come changes to the recipe. Anyone listed here in the recipe-maintainers section will have the ability to review and merge these PRs, just be sure that you only list folks here who have given explicit permission to do so.

Once we have this file created in our fork we need to commit the changes and open a Pull Request to Conda Forge. The friendly folks there will review the submission and possible ask for some tweaks and changes to ensure the recipe is good quality. Then once that has been merged your users will be able to install your package via Conda Forge.

conda install -c conda-forge is-number

Summary

In this post we have covered:

In future posts we will cover:

More in this series

This was part 3 in the Creating an open source Python project from scratch series.

Next up, Documenting your Python code

Be sure to subscribe to the RSS feed to hear about future parts in this series.


Have thoughts?

I love hearing feedback on my posts. You should head over to Twitter and let me know what you think!

Spotted a mistake? Why not suggest an edit!