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()
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"))
OUTPUT:
test
TEST
There Was A Man!
generate-a-slug-for-my-post
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
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 | |
) |
Create some more files
Let's now add a README.md
file so that setup.py
can reference it.
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
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. |
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
Since
tests
is a package, it will be included in our pip package becausesetuptools.findpackage
will add it to our pip package. This is not what we want, so let's do a quick change insetup.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") |
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
The
discover
keyword tellsunittest
to find any files with the name oftest_*.py
and run the unittests inside them.
You may have gotten an error stating:
No module named pydash
We can fix that by installing pydash using setup.py
python3 setup.py install
cd tests
python3 -m unittest discover
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
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
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/*
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
And upload on PyPI:
python3 -m twine upload dist/*
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
Top comments (1)
Part 2 is out!