Python project packaging with pipenv pytest and cli
How can I start a python project the proper way? Extensible, with tests, as library and cli application, in a virtual environment and with an older python version (raspberry pi).
I’ve spent days conquering the same problems over and over again. Notorious import problems. Imports from parent modules not working. Tests work but the cli doesn’t. Pipenv not installing the right python version. You name it.
Each time I think I’m all set, I’m encountering another missed step in my endeavour for clean code.😶
This ’essay’ shall be the base for my future python projects.
A minimal python project
Our starting point is: https://packaging.python.org/en/latest/tutorials/packaging-projects/
I’ll use the absolute minimum. E.g. pyproject.toml
contains no description nor licence etc.
Furthermore I’ve deleted files e.g. readme.md
and directories e.g. tests
.
/home/ra/tmp/$ tree packaging_tutorial
packaging_tutorial
├── pyproject.toml
└── src
└── example_package
├── example.py
└── __init__.py
3 directories, 3 files
Let’s have a look at the content of pyproject.toml
:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "example_package"
version = "0.0.1"
What’s most counterintuitive is that the name key under the [project] section in pyproject.toml must match the name of the package (example_package) and not the parent’s directory name (packaging_tutorial). In my mind the parent directory is the project’s name. In fact, the parent directory name seems completely negligible in perspective of development.
Installing the package in a virtual environment using pipenv with python 3.8
We replicate the steps from1 not from pypi
but locally from within the project’s folder (thus the dot .
after -e
).
pipenv install -e . --python 3.8
The -e
flag (editable) allows further development (hence editing) alongside the installation.
But am I really installing if using a virtual environment? At this moment I’ll leave the flag since I haven’t detected any downsides to it yet.
Ensure the virtual environment has been created
pipenv --venv
/home/ra/.local/share/virtualenvs/packaging_tutorial-okktpVF3
Activate this project’s virtualenv
pipenv shell
Ensure version 3.8 is used as virtual python environment
python -V
Check the new directory structure
tree packaging_tutorial
packaging_tutorial
├── Pipfile
├── Pipfile.lock
├── pyproject.toml
└── src
└── example_package
├── example.py
└── __init__.py
3 directories, 5 files
Let’s have a look at Pipfile’s content:
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
example-package = {file = ".", editable = true}
[dev-packages]
[requires]
python_version = "3.8"
python_full_version = "3.8.12"
Activate the virtual environment
pipenv shell
python
# Python 3.8.12 (default, Dec 4 2021, 10:54:00)
# [GCC 11.1.0] on linux
# Type "help", "copyright", "credits" or "license" for more information.
Mind that I changed the name from example_package_YOUR_USERNAME_HERE to example_package
>>> from example_package import example
>>> example.add_one(2)
Review the installation in perspective of pipenv and python 3.8
At this stage we’ve successfully replicated the package installation locally with pipenv and another python version (3.8).
Run something from the cli
Great. We’re able to import the add_one
function from the example.py
module in our virtual environment.
For better depiction I’ll add a print statement to our example.py module:
def add_one(number):
return number + 1
print(add_one(1))
Now let’s run it from the command line interface (cli)
python -m example
home/ra.local/share/virtualenvs/packaging_tutorial-okktpVF3/bin/python: No module named example
Let’s check what’s actually installed in our virtual environment
pip list
Package Version Editable project location
--------------- ------- -------------------------------
example_package 0.0.1 /home/ra/tmp/packaging_tutorial
pip 24.0
setuptools 69.2.0
wheel 0.43.0
Apparently we’re all set. Let’s try calling the module differently:
python -m src.example_package.example
This works but somehow feels wrong. Why do I need to be that explicit? What about the claimed automatic discovery2 for our src-layout3?
After further reading4, let’s try only the package name:
python -m example_package
No module named example_package.__main__; 'example_package' is a package and cannot be directly executed
At least, we got a new hint from the error, which moreover matches the setuptools instructions4. Thus, let’s make it so and add __main__.py
ImportError: cannot import name 'add_one' from 'example_package' (/home/ra/tmp/packaging_tutorial/src/example_package/__init__.py)
At this point it looks like __init__.py
is consulted due to the highlighted line 2 in __main__.py
.
Since we installed the package, we will import this package itself in our modules’s cli entrypoint __main__.py
.
Let’s change __main__.py
from example_package import example
# Works as well:
# from . import example
if __name__ == '__main__':
example.add_one(1)
Now running python -m example_package
in terminal being in /packaging_tutorial
works.
Review our current (cli) project’s state
At this point we’re able to
- pipenv install a package using an older python version
- import our package as library
- run the packages’s module from the terminal/cli
tree packaging_tutorial
packaging_tutorial
├── Pipfile
├── Pipfile.lock
├── pyproject.toml
└── src
└── example_package
├── example.py
├── __init__.py
└── __main__.py
3 directories, 6 files
-
https://packaging.python.org/en/latest/tutorials/packaging-projects/#installing-your-newly-uploaded-package ↩︎
-
https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#automatic-discovery ↩︎
-
https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#src-layout ↩︎
-
https://setuptools.pypa.io/en/latest/userguide/entry_point.html#console-scripts ↩︎ ↩︎