A while back I wrote an automatic tracklisting downloader for BBC radio shows in Python. It's simple to use and it also hooks well into get_iplayer to retrieve or tag tracklistings when you download a radio show.
One of the outstanding
issues
was to actually release it on PyPI — the official source for third party Python
packages — to make it a bit simpler to install, and to make it easier for
Python users to find. It was already packaged such that it could be installed
using pip
, but this was via GitHub rather than PyPI.
I remembered I never got round to releasing my package on PyPI and, for some reason yesterday, thought it would be a good time to see what the process was like. Maybe it'll only take half an hour, I thought, naively. It actually took me more like double to triple that time to complete the task.
Anyway, perhaps documenting this process, along with the numerous errors I encountered, might help save anyone else running up against the same painful brick walls that I did, and allow them to share their work with the Python community.
Starting out
A good starting point is a guide on the official Python site that's fairly comprehensive.
The first thing you notice when reading it is that there are lots of options
that are labelled "not recommended" and instead suggest using the
twine
package instead. This is because
older versions of Python will happily submit your PyPI credentials in plaintext
when uploading packages. More recent versions are OK, but you're best to check.
It's clearly a good thing to warn people about this. But it would be much nicer if they didn't even need such warnings in the first place. (I'm also willing to bet there's still a non-zero number of people on affected versions of Python who follow instructions elsewhere without even being aware of sending their passwords in the clear.)
Testing times
As the Python packaging guide mentions, there's a PyPI test site that you can use to test out uploading your package before you do the same on the live site.
Why's this useful? If you make mistakes in your setup and your package doesn't install, the only real recourse is to release a new version. Rather than having to create new versions, you're really best using the test PyPI site first to make sure everything's OK before you begin.
The test site looks to be an exact duplicate of the main site albeit with a
different selection of packages. This also means that you have the same
restriction as on the live site: that is, you can't replace an existing
version. You have to bump the version in setup.py
and upload it as a new
version.
I understand why this is in place for the live site. But, it's a little frustrating when testing: every change requires you to also increase the version number. Or, as I more often did, forget to do so, then see the error message telling you that the version already exists and only then increase the version number.
The details in the guide show the structure of the .pypirc
configuration.
However, they don't point out that you can also add the PyPI test site too:
[distutils]
index-servers=
pypi
testpypi
[testpypi]
repository = https://testpypi.python.org/pypi
[pypi]
repository = https://pypi.python.org/pypi
You can also add your username and password to each entry too, although you may not want it lying around in plain text in a configuration file. If you don't already have accounts on the live and test PyPI sites, this point is a good time to create them. You won't be able to progress much further otherwise.
Building the project distribution
Provided you've got setup.py
configured correctly, it's not too difficult to
build for distribution. I used the example from the PyPA sample
project as a starting point. It's also
sensible to have tagged the commit in your repository that marks the version to
be released.
The Python packaging guide linked above is also really helpful in determining
which arguments you should use with setup.py
to build your package.
If you're trying to build Python wheels, one hiccup is that you likely need the
wheel
package installed. Otherwise, you may find with a command like:
python setup.py sdist bdist_wheel
you get the error:
error: invalid command 'bdist_wheel'
Registering the project
Since twine
is recommended, it seemed sensible to use that. As my
package is for Python 2 only — since a dependency doesn't support Python
3 yet — it also seemed sensible to do the distribution upload using
Python 2.7.
The first problem I hit was running twine register dist/* -r testpypi
to
register the project. Note that you need to specify the repository using -r
if you want to use something other than the default, and the name is taken from
your .pypirc
.
Registering bbc-radio-tracklisting-downloader-0.0.1.tar.gz
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 27:
ordinal not in range(128)
That's the entirety of the error message from the register
command. There was
nothing to indicate where the problem lay at all, making it very difficult to
start fixing it.
Switching to Python 3.4 for the upload avoided the Unicode error, but instead gave me:
HTTPError: 401 Client Error: You must login to access this feature for
url: https://pypi.python.org/pypi
It still could have been a problem with the way I was inputting my password.
I'd tried both typing it at the command line -p PASSWORD
and pasting it into
the prompt which appears if no password is entered. I'd even tried entering it
directly into .pypirc
. It's possible I was just doing something wrong, but,
to me, it implies that twine
wasn't correctly handling one of the characters
in my password.
At this stage, I gave up trying at this point and went directly to the test site to register the package so I could at least make some progress and feel slightly less incompetent. That worked without a hitch.
Adding a description
On to the next problem. Unfortunately, the PyPI site doesn't support the display of Markdown READMEs for package descriptions. So, before you actually upload a package, you should provide the package description in a form that PyPI can use.
To me, Markdown always seemed much simpler than reStructuredText (reST), so I naturally gravitate to writing documentation in it. My README for this project was no different.
The main options I was aware of for solving this were:
-
specifying a
long_description
in yoursetup.py
which uses reST markup, whether read in from a file, or included in full in thesetup.py
itself, or -
converting
README.md
to a reStructuredTextREADME.rst
.
I went for the latter for simplicity.
Using pandoc
, it's pretty simple to convert an existing Markdown file:
pandoc --from=markdown --to=rst --output=README.rst README.md
On checking the output from pandoc
in an online viewer, everything
looked the same as it did when using Markdown. I also edited the README
slightly to use nicer looking markup for headings than the source Pandoc
generated.
Uploading
Whew! Back to twine
, and now the project's registered, let's try uploading it
via:
twine upload dist/* -r testpypi
I was still having the same problem as before: the 401 error. Systematically
removing unusual characters from my password until the upload worked fixed
this. (Note that the PyPI site itself was OK with me logging in with the same
password, this seemed a twine
problem, or something I was just doing wrong.)
OK. Now it's uploaded, let's check what everything looks like on the test PyPI site.
What? Where's the description taken from the README? There's nothing there at all. The README's not even present as plain text instead of formatted HTML!
What I then did first was to replace my README.rst
with one borrowed from
someone else's package that I knew displayed correctly on PyPI. Then, I
uploaded a new version of my package to the test site to make sure that I had
everything configured and uploaded correctly.
Once I'd proved that the problem was with the content of my README, I somehow instinctively figured that it was the transition markers that I'd added that were to blame. Again, this was the second time in this entire process that I had little or no indication of where the problem actually was.
Anyway, removing these transition markers, running setup.py
and uploading yet
again finally enabled my README to be correctly displayed as the package
description.
Releasing
Once that worked, all I had to do was revert the change to the version number
I'd made in setup.py
during testing, and then repeat the whole process on the
real PyPI site. Having negotiated all the obstacles the first time, I was
relieved to find this just worked.
The end result of all that is that you can finally do pip install
bbc-radio-tracklisting-downloader
instead of having to install it from GitHub.
Obviously in a process like this, there's going to be a couple of configuration hoops to jump through, especially the first time you attempt the process. Now I've been through it, I could actually probably release a new project in the thirty minutes I originally estimated.
On the other hand, the number of barriers I faced, along with the cases of little or no helpful error information was frustrating. Every stumbling block encountered is a point where someone could decide that they've had finally enough and just give up. In those cases, it's the community's loss. Developers who may well have useful packages to contribute may ultimately decide that it's not worth the trouble to release on PyPI.