← Back

Submitting a Python CLI Tool to PyPI

Last week, I built a command-line tool, csvtomd, to help me write documentation for my personal and work projects. Building Markdown tables by hand can be tedious and often involves lots of manual spacing tweaks to make the source code look just as good as the rendered table. With csvtomd, you can build a table in Excel or Numbers, export it as CSV, and convert the CSV to a Markdown table.

I wrote this tool in Python 3 and wanted to make it available to other developers. The best way to distribute Python 3 packages is to upload your package to the Python Package Index, or PyPI. Once a package is on PyPI, other developers can install your package with a simple command:

pip3 install csvtomd  

Helpful Help

Here are a few of the articles that helped me put my package online:

The most useful tutorial I found was Sharing Your Labor of Love: PyPI Quick And Dirty by Hynek Schlawack. It's a great, simple introduction to getting your Python code ready for PyPI and uploading it as a standalone package.

PyPI and CLI Scripts

One of the hiccups I discovered while trying to install my CLI app was that all the tutorials I found involved creating packages for use in other Python scripts. I had to figure out how to set the entry point to my application so that I could run it properly from the command line.

My solution was to add this entry_points line to my setup.py:

setup(  
    ...
    entry_points={
        'console_scripts': [
            'csvtomd = csvtomd:main'
        ]
    },
    ...
)

To make this entry point work, I had to modify my csvtomd.py script. Originally, my script's main module code—parsing arguments, running the conversion—was located outside a function and ran in the body of the main script. I had to move that code into a main() function.

For convenience, I added a runner for executing the script standalone:

if __name__ == '__main__':  
    main()

PyPI and Markdown

Will McKenzie came up with a nifty solution for using Markdown READMEs in PyPI packages. Here's the problem:

  • PyPI uses ReStructuredText to render READMEs
  • GitHub uses Markdown to render READMEs

Will's solution was to do the following:

  1. Create a wrapper called register.py
  2. Have register.py convert README.md (Markdown) to README.txt (ReStructuredText)
  3. Have setup.py read the README.txt file as its long_description property on PyPI

I had problems with the pyandoc library, so I took Will's idea and rebuilt it slightly differently into setup_wrap.py:

import sys  
import os  
import subprocess  
import pypandoc

with open('README.rst', 'w') as dest:  
    long_description = pypandoc.convert('README.md', 'rst')
    dest.write(long_description)

args = ['python3', 'setup.py'] + sys.argv[1:]  
subprocess.call(args)

os.remove('README.rst')  

setup_wrap.py does the following:

  1. Converts README.md to README.rst using pypandoc
  2. Runs python3 setup.py and passes along its own arguments
  3. Deletes README.rst after setup.py is done

Additionally, I added the following to setup.py:

with open('README.rst') as f:  
    long_description = f.read()

setup(  
    ...
    long_description=long_description,
    ...
)

So now, instead of running python3 setup.py sdist, just run python3 setup_wrap.py sdist and you'll have a very pretty README on both GitHub and PyPI.