Here we present how one can organize python project as a package. We will create sample package sample-package.

There is a great guide to packages, namely The Hitchhiker’s Guide to Packaging. We recommend start form there. Here we are definitely build on it, but we do few significant changes.

Our package will generate random number.

Directory organization

We will do a bit not standard, however very popular, package organization. It makes setup.py a bit more complicated, but still perfectly functional. We also move all requirements need to requirements.txt and requirements.test.txt files. This makes the code for development more DRY.

The structure of package is the following:

samplePackage/
    Makefile
    README.md
    requirements.txt
    requirements.test.txt
    setup.py
    setup.cfg
    src/
        samplePackage/
            __init__.py
            normal_number_generator.py
    tests/
        

Setup file

Let’s create setup.py file first.

import os
import sys

from setuptools import setup, find_packages

base_dir = os.path.dirname(__file__)
src_dir = os.path.join(base_dir, 'src')
sys.path.insert(0, src_dir)

import samplePackage

def get_requirements(requirements_path='requirements.txt'):
    with open(requirements_path) as fp:
        return [x.strip() for x in fp.read().split('\n') if not x.startswith('#')]


setup(
    name='samplePackage',
    version=samplePackage.__version__,
    description='Sample Python Package',
    author='bartek',
    author_email='bartekskorulski@gmail.com',
    packages=find_packages(where='src', exclude=['tests']),
    package_dir={'': 'src'},
    install_requires=get_requirements(),
    setup_requires=['pytest-runner', 'wheel'],
    testsp_require=get_requirements('requirements.test.txt'),
    url='https://github.com/sbartek/sample-package',
    classifiers=[
        'Programming Language :: Python :: 3.7.7'
    ],
)

Next, let’s make this setup work. So create directory src and inside of it samplePackage. Then, inside it file __init__.py with

__version__ = '0.0.1'

Then create requirements.txt with:

numpy==1.18.2

and requirements.test.txt with

pytest==5.4.1
pytest-cov==2.8.1
flake8==3.7.9

Now you should be able to run the following:

pip install -e .
python -c "import samplePackage"

and you should not see any error.

Note that we have install package in editable mode.

Tests

Create directory tests/ and inside file test_simple_package.py with the following:

from unittest import TestCase
from samplePackage.normal_number_generator import NormalNumberGenerator

class TestNormalNumberGenerator(TestCase):

    def setUp(self):
        seed = 42
        self.normal_number_generator = NormalNumberGenerator(seed=seed)

    def test_generate(self):
        self.assertAlmostEqual(self.normal_number_generator.generate(), 0, places=4)

Let’s install test requirements by

pip install -r requirements.test.txt

Now run tests:

pytest

Of course test should fail since we still did not implemented normal_number_generator. So let’s do it now.

The module

Inside src/samplePackage/ create normal_number_generator.py file with the following code.

import numpy as np


class NormalNumberGenerator:

    def __init__(self, seed=None):
        if seed is not None:
            np.random.seed(seed=seed)

    def generate(self):
        return np.random.normal()

Now when you run test you should have an error like:

E       AssertionError: 0.4967141530112327 != 0 within 4 places (0.4967141530112327 difference)

Copy then first 4 digits 0.4967 into tests/test_simple_package.py by changig the assert into:

        self.assertAlmostEqual(self.normal_number_generator.generate(), 0.4967, places=4)

flake8

flake8 is a tool for enforcing style consistency across Python projects. You can run it by:

flake8

You should see some errors. In particular, it tells that lines are too long. We would rather allow to have lines a bit longer (100 characters). For this we create file setup.cfg with the following:

[flake8]
exclude =
        setup.py,
        .eggs/
max-line-length = 100

If there are more errors please remove them.

Coverage

You can also get report about coverage of your tests. You can get them by running:

pytest --cov=samplePackage tests/

Makefile

You can automatize some task by using Makefile. Create it with the following content:

PIP := pip3

.PHONY: help
help: ## display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)

.PHONY: init
init: ## install package
	${PIP} install -r requirements.txt
	${PIP} install -r requirements.test.txt
	${PIP} install .

.PHONY: python_tests
python_tests: ## run unit tests
	pytest --cov=samplePackage tests/
	flake8 

Now you can run test by:

make python_tests

Moreover, when you download and need to install package you can do this by:

make init

Do not forget to add README.md with instructions how to install the package.

Code

Code is available at https://github.com/sbartek/sample-package

Updated: 2020-04-12