DEV Community

arnu515
arnu515

Posted on • Edited on

7 1

Create a PyPI (pip) package, test it and publish it using Github Actions (PART 1)

Creating a PyPI package is easier than you think, because all you have to do is create a python package, a setup.py file that defines your package and upload it on PyPI (The Python Package Index) for millions of users to install via pip.

Writing our code

First, we need to create a python package. I have a very simple package in mind, one that allows you to interact and manipulate strings, like lodash.

I'm gonna be using PyCharm as my IDE of choice, but feel free to use anything you want (VSCode, Spyder, IDLE, Vim, etc).

My folder will be named pydash, as it is the name of my pip package. Inside the pydash folder, let's create another folder called pydash. This new pydash folder will house all of our code. Create an __init__.py file in that folder. We'll be editing that file.

class PyDash:
    def __init__(self):
        pass

    @staticmethod
    def lower(string: str):
        """
        Converts a string to lowercase
        """
        return string.lower()

    @staticmethod
    def upper(string: str):
        """
        Converts a string to uppercase
        """
        return string.upper()

    @staticmethod
    def title(string: str):
        """
        Converts a string to titlecase
        """
        return string.title()

    @staticmethod
    def kebab(string: str):
        """
        Converts a string to kebabcase
        """
        return string.replace(" ", "-").lower()
Enter fullscreen mode Exit fullscreen mode

Testing our package

Create a python file named test.py, index.py or whatever you want OUTSIDE the pydash folder (the one containing __init__.py. We can import and test our package like so:

from pydash import PyDash

print(pydash.lower("TEST"))
print(pydash.upper("test"))
print(pydash.title("there WAS a MAN!"))
print(pydash.kebab("Generate a Slug for my Post"))
Enter fullscreen mode Exit fullscreen mode
OUTPUT:
test
TEST
There Was A Man!
generate-a-slug-for-my-post
Enter fullscreen mode Exit fullscreen mode

Adding a setup.py

Our package works great! You can now delete your test python file. We're almost ready to get this package up and running on PyPI. There's just one problem. PyPI knows nothing about our project, so, let's tell it something. Create a file named setup.py in your project directory. This directory listing should help:

- pydash
| - pydash
  | - __init__.py
| - setup.py
Enter fullscreen mode Exit fullscreen mode

First, we have to decide on a name that is not taken on PyPI. Unfortunately for me, PyDash already exists, so I'm gonna go with the name pydash-arnu515.

Once you've gotten setup with your name, let's add some stuff to setup.py

from setuptools import setup, find_packages
from os.path import abspath, dirname, join
# Fetches the content from README.md
# This will be used for the "long_description" field.
README_MD = open(join(dirname(abspath(__file__)), "README.md")).read()
setup(
# The name of your project that we discussed earlier.
# This name will decide what users will type when they install your package.
# In my case it will be:
# pip install pydash-arnu515
# This field is REQUIRED
name="pydash-arnu515",
# The version of your project.
# Usually, it would be in the form of:
# major.minor.patch
# eg: 1.0.0, 1.0.1, 3.0.2, 5.0-beta, etc.
# You CANNOT upload two versions of your package with the same version number
# This field is REQUIRED
version="1.0.0",
# The packages that constitute your project.
# For my project, I have only one - "pydash".
# Either you could write the name of the package, or
# alternatively use setuptools.findpackages()
#
# If you only have one file, instead of a package,
# you can instead use the py_modules field instead.
# EITHER py_modules OR packages should be present.
packages=find_packages(),
# The description that will be shown on PyPI.
# Keep it short and concise
# This field is OPTIONAL
description="A small clone of lodash",
# The content that will be shown on your project page.
# In this case, we're displaying whatever is there in our README.md file
# This field is OPTIONAL
long_description=README_MD,
# Now, we'll tell PyPI what language our README file is in.
# In my case it is in Markdown, so I'll write "text/markdown"
# Some people use reStructuredText instead, so you should write "text/x-rst"
# If your README is just a text file, you have to write "text/plain"
# This field is OPTIONAL
long_description_content_type="text/markdown",
# The url field should contain a link to a git repository, the project's website
# or the project's documentation. I'll leave a link to this project's Github repository.
# This field is OPTIONAL
url="https://github.com/arnu515/pydash",
# The author name and email fields are self explanatory.
# These fields are OPTIONAL
author_name="arnu515",
author_email="arnu5152@gmail.com",
# Classifiers help categorize your project.
# For a complete list of classifiers, visit:
# https://pypi.org/classifiers
# This is OPTIONAL
classifiers=[
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3 :: Only"
],
# Keywords are tags that identify your project and help searching for it
# This field is OPTIONAL
keywords="lodash, string, manipulation",
# For additional fields, check:
# https://github.com/pypa/sampleproject/blob/master/setup.py
)
view raw setup.py hosted with ❤ by GitHub

Create some more files

Let's now add a README.md file so that setup.py can reference it.

Pydash

A simple and stupid clone of LoDash in python.

This was made for my blog post on dev.to and is not a serious package, so don't expect updates.

view raw README.md hosted with ❤ by GitHub

Let's add a .gitignore, so that we don't commit any files we aren't supposed to.

__pycache__/
build/
dist/
*.egg-info/
*.egg
venv
Enter fullscreen mode Exit fullscreen mode

I recommend marking these directories as excluded in PyCharm to avoid getting errors like "Duplicated Code Fragment".

Now, one last file, the LICENSE. In setup.py, I set my license to be MIT, so that's what I'm going for.

MIT License
Copyright (c) 2021 arnu515
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
view raw LICENSE hosted with ❤ by GitHub

And that's it for our python package!

Testing our package with unittest.

Unittest is a builtin python module that allows us to test our package. This blog post doesn't introduce unittest to you. For that, there are many more tutorials online. Here, I'll just show you how you can test your python package.

Creating our tests.

Create a folder named tests in the project folder and create a file named __init__.py in it. This should be how your folder looks like:

- pydash
| - pydash
  | - __init__.py
| - tests
  | - __init__.py
| - setup.py
Enter fullscreen mode Exit fullscreen mode

Since tests is a package, it will be included in our pip package because setuptools.findpackage will add it to our pip package. This is not what we want, so let's do a quick change in setup.py

packages=find_packages(exclude="tests")

Adding our first test

Create a file named test_string.py.

import unittest
from pydash import PyDash
class Test(unittest.TestCase):
def test_lower_method(self):
self.assertEqual(PyDash.lower("TEST"), "test")
self.assertNotEqual(PyDash.lower("test"), "TEST")
def test_upper_method(self):
self.assertEqual(PyDash.upper("test"), "TEST")
self.assertNotEqual(PyDash.upper("TEST"), "test")
def test_title_method(self):
self.assertEqual(PyDash.title("hello world"), "Hello world")
self.assertNotEqual(PyDash.title("hELLO wORLD"), "hello world")
def test_kebab_method(self):
self.assertEqual(PyDash.kebab("Kebab case adds hyphens BetWEEN lowerCASE text"),
"kebab-case-adds-hyphens-between-lowercase-text")
self.assertNotEqual(PyDash.kebab("Kebab case doesn't contain spaces"), "kebab-case-doesn't contain spaces")
view raw test_string.py hosted with ❤ by GitHub

PyCharm allows me to run unittests automatically, but I'm not going to do that, since not everyone uses PyCharm. Instead, we'll use the command line to test our app.

cd tests
python3 -m unittest discover
Enter fullscreen mode Exit fullscreen mode

The discover keyword tells unittest to find any files with the name of test_*.py and run the unittests inside them.

You may have gotten an error stating:

No module named pydash
Enter fullscreen mode Exit fullscreen mode

We can fix that by installing pydash using setup.py

python3 setup.py install
cd tests
python3 -m unittest discover
Enter fullscreen mode Exit fullscreen mode

Your tests should've run successfully.

Uploading to TestPyPI

Now, for the fun, wait, what is TestPyPI? TestPyPI is a separate instance of PyPI meant to test your packages first, so let's upload our package there first.

We will need to install a few packages first.

pip install twine wheel
Enter fullscreen mode Exit fullscreen mode

We need to create an account on TestPyPI first. Head over to TestPyPI and create an account (or login if you have one already). You will need to verify your email address in order to upload a package.

Create an access token

Let's create an access token which will allow us to upload to TestPyPI without having to type in our email and password all the time. Go to your API tokens in Account Settings and create a full access API token. Give it a name and set its scope to Entire Account.

Copy your API Token and store it in a safe place once it is displayed to you, as it will never be displayed to you again

It is VERY important that you don't share your API token and don't commit it to git because anyone with access to that token can upload packages to your account.

Building and Uploading your package to TestPyPI

Now comes the fun! Let's build your package using this command:

python3 setup.py sdist bdist_wheel
Enter fullscreen mode Exit fullscreen mode

This should create a new folder called dist in your current directory.

Let's upload that folder using twine:

python3 -m twine upload -r testpypi dist/*
Enter fullscreen mode Exit fullscreen mode

For the username enter __token__, and for the password, paste your API Token.

Voila! Your package has successfully been uploaded to TestPyPI!

View it at https://test.pypi.org/project/YOUR_PROJECT_NAME

For example, mine would be https://test.pypi.org/project/pydash-arnu515

Uploading to PyPI

Now that we've uploaded our package to TestPyPI, we can do the same thing to regular PyPI. Create an account on PyPI.org and then create an API Token.

Build your package again:

python3 setup.py sdist bdist_wheel
Enter fullscreen mode Exit fullscreen mode

And upload on PyPI:

python3 -m twine upload dist/*
Enter fullscreen mode Exit fullscreen mode

For the username enter __token__, and for the password, paste your API Token from PyPI.

And we're done

Congratulations! You have a new package up and running on PyPI. For automation with Github Actions, view Part 2

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (1)

Collapse
 
arnu515 profile image
arnu515

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs