Publishing my open source project to PyPI!
Tags: python, pypi, opensource, pip
Published: 2023-11-25 03:07:59

This week I released my Python open source project **[go-go-web](https://github.com/kliu57/go-go-web)** using **[PyPI](https://pypi.org/)**! This means users no longer have to clone my repo in order to run my app. One simple `pip install gogoweb` command can install the app for anyone!

Here's the process for releasing Python software using PyPI.

If you prefer to watch a video, I recommend [PyPi - How to Publish your own PIP package](https://www.youtube.com/watch?v=v4bkJef4W94) by [DevOpsJourney](https://www.youtube.com/@DevOpsJourney). In addition, please see [official Python documentation on packaging projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/).

## 1) Create a PyPI account and create an API token

- Register at [PyPI.org](https://pypi.org/)
- Generate recovery codes (mandatory step)
- Activate Two Factor Authentication (mandatory step)
- Generate API token (save the value!)

## 2) Install / upgrade package manager, build, publishing tools

Windows
```
py -m pip install --upgrade pip
py -m pip install --upgrade build
py -m pip install --upgrade twine
```

Linux/MAC OS
```
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade build
python3 -m pip install --upgrade twine
```

## 3) Restructure your project to have the following structure

```
project_root/
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.cfg
├── src/
│   └── package_name/
│       ├── __init__.py
│       └── programcode.py
└── tests/
```

`package_name/` : Name this directory the name you would like your software to have when users do `pip install`. For example, I used `gogoweb`, so users use `pip install gogoweb`. This name must not be already taken.

To check if a name is taken:

- run `pip install package_name` to check if a package installs, followed by `pip uninstall package_name`
- or, check if the url exists, i.e. https://pypi.org/project/package_name

There must be an `__init__.py` file in the `package_name` folder.

All source files and referenced files for running your project should be inside the `package_name` directory.

This is my project structure:

![project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vs5azl1yhfypv8rx3t3h.png)

## 4) Make sure your program still runs successfully on local after restructuring

I had to make changes to my source code in order for the package to run properly:

- When importing modules from the current directory, changed<br>`import module_name` to `from . import module_name`
  - This was a fix I did later after releasing my package, because I found that the packaged file, when run, would try to import from the current working directory instead of where the packaged file is situated unless I employ this fix

<br><br>
- Changed the logic in my code to output files by default to a `til` folder in the current working directory instead of the source code folder
  - This is for user convenience, because the installed package will be in an obscure location like `C:\Users\katie\AppData\Local\Programs\Python\Python312\Lib\site-packages\gogoweb` and users probably won't want to navigate there to get their output files

<br><br>
- Updated the import statements in my unit test files and unit test config file since the paths of source files changed<br><br>

- Updated the pylint command in my CI Workflow .yml file since the directory containing the source files to be linted changed

## 5) Contents of pyproject.toml

```
[build-system]
requires = [
    "setuptools>=42",
    "wheel"
]
build-backend = "setuptools.build_meta"

[project]
name = "gogoweb"
authors = [
    {name = "Katie Liu", email = "liu_katie@outlook.com"}
]
readme = "README.md"
requires-python = ">=3.7"
classifiers = ["License :: OSI Approved :: MIT License"]
dynamic = ["version", "description"]

dependencies = [
    "tomlkit",
    "python-frontmatter"
]

[project.urls]
Homepage = "https://github.com/kliu57/go-go-web"
Issues = "https://github.com/kliu57/go-go-web/issues"
```

Not everything I have in `pyproject.toml` is mandatory. The mandatory items are:
- [build-system] section
- [project] section - dependencies (Packages that need to be installed for your program to run successfully. I.e. my program requires the user to do `pip install tomlkit` and `pip install python-frontmatter` before running the program.)

## 6) Contents of setup.cfg

Replace with your own project values.

```
[metadata]
name = gogoweb
version = 1.0.3
author = Katie Liu
author_email = liu_katie@outlook.com
description = Command-line tool that converts .txt or .md files into .html files.
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/kliu57/go-go-web
project_urls =
    Bug Tracker = https://github.com/kliu57/go-go-web/issues
classifiers =
    Programming Language :: Python :: 3
    License :: OSI Approved :: MIT License
    Operating System :: OS Independent

[options]
package_dir =
    = src
packages = find:
python_requires = >=3.7

[options.packages.find]
where = src
```

## 7) Create the build

Windows
```
py -m build
```

Linux/MAC OS
```
python3 -m build
```

After you run this command, you should get a "Successfully built" message with a file name that corresponds to the version number you specified in the setup.cfg. Check that the newly created `dist/` directory contains that file. If not, troubleshoot before continuing further.

![dist directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/un6m84ac43xrhcs7ao2t.png)

## 8) Upload your package

Windows
```
py -m twine upload dist/*
```

Linux/MAC OS
```
python3 -m twine upload dist/*
```

You will be prompted to enter a username and password. For the username, enter `__token__`. For the password, copy and then paste your PyPI API token value from Step 1 (On a terminal you can right click to paste).


Successful upload to PyPI:

![upload](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9j3xxznvrgwm0biop7c.png)

Once uploaded, my project has its own [PyPI website](https://pypi.org/project/gogoweb/)!

![PyPI site](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6phddlwzefmbc618st5r.png)

Now that the project is uploaded to `PyPI`, anyone can download it with a simple `pip install` command:

Windows
```
pip install gogoweb
```

Linux/MAC OS
```
python3 -m pip install gogoweb
```

I can check details of the installation and where the files were installed to:

Windows
```
pip show gogoweb
```

Linux/MAC OS
```
python3 -m pip show gogoweb
```

![pip show](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k51e56uts6jpby29f75n.png)

After installing, my command-line app can be run with this command.

Windows
```
py -m gogoweb.convert <file or folder path>
```

Linux/MAC OS
```
python3 -m gogoweb.convert <file or folder path>
```

This will take the file or folder and convert any `.txt` and `.md` files into `.html` files. Default output location is a `til` folder in the current working directory. See [go-go-web documentation](https://github.com/kliu57/go-go-web#readme) for more details.

![powershell run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uujhvonkwmbvaxfd9gru.png)

![input and output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kehnv0ua2h5ars9zwg8s.png)

---

# Run your package and more

Notice that `gogoweb` the name of my package and `convert` is the name of the `.py` file I want to run. For your package, if you wanted to run a python file from that package:

Windows
```
py -m <your package>.<your file> [args]
```

Linux/MAC OS
```
python3 -m gogoweb.convert <your package>.<your file> [args]
```

There are many things you could do with your package once installed. For example you could call certain functions that are inside your package:

```
from <package_name> import <file_name> as pf

pf.somefunction()
```

---

# Reuploading a package

- Increment the version number in `setup.cfg`
- Repeat step 7 to recreate the build
- Repeat step 8 to upload the build

---

# User Testing

[Roy](https://dev.to/rjwignar) and [Amnish](https://dev.to/amnish04) were kind enough to test my app installation and run! Amnish had no issues with the app, and Roy helped me find and find a fix for a unique issue arising from multiple version of Python installed on his machine. The `pip install` installed the app to his Python v3.11 directory, while his terminal python run command is looking for the app in the Python v3.12 directory. 

![Error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1t0h6zqh1qbx9j82sjip.png)

The fix for this is to first find out what Python directory the app was installed to with `pip show gogoweb`

![pip show](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3h2fsn331ohxhch0egs3.png)

Then we specify a Python version with our run command:

```
py -<version number> -m gogoweb.convert [args]
```

![specifying python version run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/srni1kz3te4su0ke9q44.png)

---

I am very happy to have completed my first software release. Users can now read instructions to install my program with pip on my [repo](https://github.com/kliu57/go-go-web)!