From script to project#

Typical steps#

  1. Organize your script into a package and modules

  2. Add install scripts/instructions

  3. Add documentation and make it available online

  4. Add tests

  5. Deploy your application/library

The final version of this lecture are available here:

Source code: UiO-IN3110/monty_hall_game

Online documentation: https://uio-monty-hall-pre.readthedocs.io

Automatic testing: UiO-IN3110/monty_hall_game

Our test case#

We will use the Monty Hall Game implementation from last week as an example.

../../../_images/monty_hall_game.png

This project currently consists of the game file itself and some html templates:

monty_hall_game/
    game_server.py     # Start web server
    templates/*.html   # Templates for web-server

Goal: Make the game available as a project with:

  • a command line interface

  • a web interface

  • online and offline documentation

  • tests

  • error handling.

Step 1. Organize your script into modules and functions.#

We would like to offer a command line interface and a web interface. To achieve this, we separate the game logic into a separate package (which is simply a directory with Python modules).

myproject/
    monty_hall_game/               # Game package
        __init__.py                    #    Init file module
        monty_hall_game.py             #    Core game module

The core game module contains the class MontyHallGame, which implements the core functionality of the game. Here is a the user interface for the game package:

Let’s look at the user interface of the package.

Setting up the game:

!tree monty-hall-game0
monty-hall-game0
├── game_server.py
└── templates
    ├── final.html
    ├── reselect1.html
    ├── reselect.html
    ├── select2.html
    └── select.html

1 directory, 6 files
!tree -I __pycache__ monty-hall-game1
monty-hall-game1
├── bin
│   ├── play_monty_hall_cli.py
│   └── play_monty_hall_web.py
├── monty_hall_game
│   ├── __init__.py
│   └── monty_hall_game.py
└── templates
    ├── final.html
    ├── reselect1.html
    ├── reselect.html
    ├── select2.html
    └── select.html

3 directories, 9 files
import os
import sys

sys.path.insert(0, os.path.join(os.getcwd(), "monty-hall-game1"))
import monty_hall_game

game = monty_hall_game.MontyHallGame()

We can now play one round:

game.select_door(door=1)
game.let_host_open_door()
3
game.select_door(1)
game.open_door()  # True == win, False == lose
False

Printing the game statistics:

print(game.statistics())
Changed and won: 0 out of 0
Not changed and won: 0 out of 1

With this package, we can build scripts that expose the game to the user via the command line and the web-interface. We implement these in the folder bin (for binary files. We use this name of convention reasons, even though our files are not really bindary files).

Our new directory structure is:

myproject/
    bin/                           # Scripts
        play_monty_hall_cli.py         #    Start game with command line interface
        play_monty_hall_web.py         #    Start game with web-server
    monty_hall_game/               # Game package
        __init__.py                    #    Init file module
        monty_hall_game.py             #    Core game module
    templates/*.html               # Templates for web-server

Let’s look at the code in more detail (see files in monty-hall-game1 folder)

Step 2. Add installation files and instructions#

Record dependencies with requirements.txt#

Our project has some dependencies to run it, such as flask, and other dependencies to “develop” it such as pytest and (later) sphinx:

!cat monty-hall-game4/requirements.txt
flask
!cat monty-hall-game4/dev-requirements.txt
pytest
sphinx

The dependencies can be automatically installed with

%pip install -r monty-hall-game4/requirements.txt -r monty-hall-game4/dev-requirements.txt
Requirement already satisfied: flask in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from -r monty-hall-game4/requirements.txt (line 1)) (3.0.0)
Requirement already satisfied: pytest in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from -r monty-hall-game4/dev-requirements.txt (line 1)) (7.4.3)
Requirement already satisfied: sphinx in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from -r monty-hall-game4/dev-requirements.txt (line 2)) (7.2.6)
Requirement already satisfied: click>=8.1.3 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->-r monty-hall-game4/requirements.txt (line 1)) (8.1.7)
Requirement already satisfied: Werkzeug>=3.0.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->-r monty-hall-game4/requirements.txt (line 1)) (3.0.1)
Requirement already satisfied: itsdangerous>=2.1.2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->-r monty-hall-game4/requirements.txt (line 1)) (2.1.2)
Requirement already satisfied: blinker>=1.6.2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->-r monty-hall-game4/requirements.txt (line 1)) (1.7.0)
Requirement already satisfied: Jinja2>=3.1.2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->-r monty-hall-game4/requirements.txt (line 1)) (3.1.2)
Requirement already satisfied: packaging in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from pytest->-r monty-hall-game4/dev-requirements.txt (line 1)) (23.2)
Requirement already satisfied: exceptiongroup>=1.0.0rc8 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from pytest->-r monty-hall-game4/dev-requirements.txt (line 1)) (1.1.3)
Requirement already satisfied: iniconfig in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from pytest->-r monty-hall-game4/dev-requirements.txt (line 1)) (2.0.0)
Requirement already satisfied: tomli>=1.0.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from pytest->-r monty-hall-game4/dev-requirements.txt (line 1)) (2.0.1)
Requirement already satisfied: pluggy<2.0,>=0.12 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from pytest->-r monty-hall-game4/dev-requirements.txt (line 1)) (1.3.0)
Requirement already satisfied: alabaster<0.8,>=0.7 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (0.7.13)
Requirement already satisfied: sphinxcontrib-htmlhelp>=2.0.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2.0.4)
Requirement already satisfied: docutils<0.21,>=0.18.1 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (0.20.1)
Requirement already satisfied: requests>=2.25.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2.31.0)
Requirement already satisfied: babel>=2.9 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2.13.1)
Requirement already satisfied: sphinxcontrib-applehelp in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (1.0.7)
Requirement already satisfied: sphinxcontrib-jsmath in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (1.0.1)
Requirement already satisfied: sphinxcontrib-qthelp in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (1.0.6)
Requirement already satisfied: sphinxcontrib-devhelp in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (1.0.5)
Requirement already satisfied: Pygments>=2.14 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2.16.1)
Requirement already satisfied: sphinxcontrib-serializinghtml>=1.1.9 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (1.1.9)
Requirement already satisfied: imagesize>=1.3 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (1.4.1)
Requirement already satisfied: snowballstemmer>=2.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2.2.0)
Requirement already satisfied: MarkupSafe>=2.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask->-r monty-hall-game4/requirements.txt (line 1)) (2.1.3)
Requirement already satisfied: urllib3<3,>=1.21.1 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from requests>=2.25.0->sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2.1.0)
Requirement already satisfied: idna<4,>=2.5 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from requests>=2.25.0->sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (3.4)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from requests>=2.25.0->sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (3.3.2)
Requirement already satisfied: certifi>=2017.4.17 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from requests>=2.25.0->sphinx->-r monty-hall-game4/dev-requirements.txt (line 2)) (2023.7.22)
Note: you may need to restart the kernel to use updated packages.

Setuptools#

Further, we can create a setup file to simplify the installation of our game. First thing we need is a pyproject.toml file:

%pycat monty-hall-game4/pyproject.toml
[build-system]
requires = [
    "setuptools",
]
build-backend = "setuptools.build_meta"

[project]
version = "0.1.0"
requires-python = ">=3.8"
license = {text = "MIT License"}

name = "monty_hall_game"
description = "..."
readme = "README.md"
dynamic = ["dependencies", "optional-dependencies"]


[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt",]}
optional-dependencies.dev = { file = ["dev-requirements.txt"] }

[project.scripts]
monty-hall-cli = "monty_hall_game.cli:main"
monty-hall-web = "monty_hall_game.web:main"

We note that we here have used a few new features compared to what we covered in Structuring Python code.

  • dynamic: Some fields can be modified by other packages, scripts etc. when installing. One example is the list of dependencies, that we now let setuptools fetch from requirements.txt. See Dynamic metadata for more information on this. Similarly, we do the same thing for the [dev] extensions that are optional dependencies, only used to build the web client, but not run the monty_hall_game.

  • [project-scripts]: creates an alias from calling the monty_hall_game.cli.main() function from the command line as monty-hall-cli. The syntax is package.submodule:function.

We can now install the monty_hall_game package with

python3 -m pip install .

Using the Python package manager pip has the advantage that we can uninstall the package again:

python3 -m pip uninstall monty_hall_game
!tree monty-hall-game4/
monty-hall-game4/
├── build
│   ├── bdist.linux-x86_64
│   └── lib
│       └── monty_hall_game
│           ├── cli.py
│           ├── game_exceptions.py
│           ├── __init__.py
│           ├── __main__.py
│           ├── monty_hall_game.py
│           └── web.py
├── dev-requirements.txt
├── docs
│   ├── api
│   │   ├── index.rst
│   │   └── monty_hall_game.rst
│   ├── _build
│   │   ├── doctrees
│   │   │   ├── api
│   │   │   │   ├── index.doctree
│   │   │   │   └── monty_hall_game.doctree
│   │   │   ├── environment.pickle
│   │   │   └── index.doctree
│   │   └── html
│   │       ├── api
│   │       │   ├── index.html
│   │       │   └── monty_hall_game.html
│   │       ├── genindex.html
│   │       ├── index.html
│   │       ├── _modules
│   │       │   ├── index.html
│   │       │   └── monty_hall_game
│   │       │       ├── game_exceptions.html
│   │       │       └── monty_hall_game.html
│   │       ├── objects.inv
│   │       ├── py-modindex.html
│   │       ├── search.html
│   │       ├── searchindex.js
│   │       ├── _sources
│   │       │   ├── api
│   │       │   │   ├── index.rst.txt
│   │       │   │   └── monty_hall_game.rst.txt
│   │       │   └── index.rst.txt
│   │       └── _static
│   │           ├── alabaster.css
│   │           ├── basic.css
│   │           ├── custom.css
│   │           ├── doctools.js
│   │           ├── documentation_options.js
│   │           ├── file.png
│   │           ├── language_data.js
│   │           ├── minus.png
│   │           ├── plus.png
│   │           ├── pygments.css
│   │           ├── searchtools.js
│   │           └── sphinx_highlight.js
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   └── Makefile
├── monty_hall_game
│   ├── cli.py
│   ├── game_exceptions.py
│   ├── __init__.py
│   ├── __main__.py
│   ├── monty_hall_game.py
│   ├── __pycache__
│   │   ├── game_exceptions.cpython-310.pyc
│   │   ├── __init__.cpython-310.pyc
│   │   └── monty_hall_game.cpython-310.pyc
│   ├── static
│   │   ├── car.jpg
│   │   ├── door1.png
│   │   ├── door2.png
│   │   ├── door3.png
│   │   ├── goat.jpg
│   │   ├── goat_lost.jpg
│   │   └── winner.png
│   ├── templates
│   │   ├── final.html
│   │   ├── layout7.html
│   │   ├── reselect.html
│   │   └── select.html
│   └── web.py
├── monty_hall_game.egg-info
│   ├── dependency_links.txt
│   ├── entry_points.txt
│   ├── PKG-INFO
│   ├── requires.txt
│   ├── SOURCES.txt
│   └── top_level.txt
├── pyproject.toml
├── README.md
├── readthedocs.yml
├── requirements.txt
├── runtime.txt
└── tests
    ├── __pycache__
    │   └── test_game_module.cpython-310-pytest-7.4.3.pyc
    └── test_game_module.py

23 directories, 76 files

Installation instructions#

Finally it is good practice to add a installation instructions to the README.md file:

from IPython.display import Markdown, display

with open("monty-hall-game4/README.md") as f:
    display(Markdown(f.read()))

Monty Hall Game

This repository contains a simple implementation of the Monty Hall Game with a command line and web interface.

Installation

Install the game with

python3 -m pip install .

Running the game

The command line interface is started with:

monty-hall-cli

or with

python3 -m monty_hall_game

The web server is started with:

monty-hall-web

Documentation

The documentation is available online, or can be built locally with

cd docs
make html
firefox _build/html/index.html

New files#

monty_hall_game/
    README.md                   # Installation instructions
    requirements.txt            # List of project dependencies
    dev-requirements.txt        # List of development dependencies
    pyproject.toml              # SetupTools file
!pwd
/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project
%cd monty-hall-game4/
%pip install .
/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4
Processing /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4
  Installing build dependencies ... ?25ldone
?25h  Getting requirements to build wheel ... ?25ldone
?25h  Installing backend dependencies ... ?25ldone
?25h  Preparing metadata (pyproject.toml) ... ?25ldone
?25hRequirement already satisfied: flask in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from monty-hall-game==0.1.0) (3.0.0)
Requirement already satisfied: itsdangerous>=2.1.2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->monty-hall-game==0.1.0) (2.1.2)
Requirement already satisfied: Jinja2>=3.1.2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->monty-hall-game==0.1.0) (3.1.2)
Requirement already satisfied: Werkzeug>=3.0.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->monty-hall-game==0.1.0) (3.0.1)
Requirement already satisfied: blinker>=1.6.2 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->monty-hall-game==0.1.0) (1.7.0)
Requirement already satisfied: click>=8.1.3 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from flask->monty-hall-game==0.1.0) (8.1.7)
Requirement already satisfied: MarkupSafe>=2.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask->monty-hall-game==0.1.0) (2.1.3)
Building wheels for collected packages: monty-hall-game
  Building wheel for monty-hall-game (pyproject.toml) ... ?25ldone
?25h  Created wheel for monty-hall-game: filename=monty_hall_game-0.1.0-py3-none-any.whl size=5515 sha256=267eda1931ab96387337dfbd972aecb677e1545af65dc49bffd5fd899fb2b463
  Stored in directory: /home/dokken/.cache/pip/wheels/09/58/ce/9a5cb9c9e406dd150c303a550c32272fea1f7295aefd30c59a
Successfully built monty-hall-game
Installing collected packages: monty-hall-game
  Attempting uninstall: monty-hall-game
    Found existing installation: monty-hall-game 0.1.0
    Uninstalling monty-hall-game-0.1.0:
      Successfully uninstalled monty-hall-game-0.1.0
Successfully installed monty-hall-game-0.1.0
Note: you may need to restart the kernel to use updated packages.

Step 3. Add documentation#

We should add docstrings to the module monty_hall_game.py file.

Note, that I write the documentation in a Sphinx Markup Style (see https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) to obtain nicely rendered online documentation.

Let’s look at the code in more detail (see files in monty-hall-game2 folder)

Once done, we can access the docstrings as usual:

# update import path and re-import monty_hall_game
sys.path.insert(0, os.path.join(os.getcwd(), "monty-hall-game2"))
from importlib import reload

reload(monty_hall_game)
reload(monty_hall_game.monty_hall_game)
reload(monty_hall_game)

from monty_hall_game.monty_hall_game import MontyHallGame
MontyHallGame.let_host_open_door?
Signature: MontyHallGame.let_host_open_door(self)
Docstring: <no docstring>
File:      ~/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game1/monty_hall_game/monty_hall_game.py
Type:      function

Python documentation with Sphinx#

Sphinx is a powerful tool to create documentation for Python projects and provides more flexibiliy.

Installation#

python3 -m pip install sphinx

How to get started#

  1. Use the quick start command to configure a base Sphinx documentation

    sphinx-quickstart
    

    Among other things, the quickstart guide will ask for the documentation folder. I typically choose docs for this.

  2. Use

    mkdir -p docs/api
    sphinx-apidoc -o docs/api monty_hall_game
    

    to add documentation for each module.

  3. Edit docs/index.rst to change the content of your main page.

  4. Compile the documentation with:

    cd docs
    make html
    

    (make sure that the module is in sys.path or installed).

  5. The documentation is available on docs/_build/html/index.html.

%cd docs
!make html
/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4/docs
sphinx-build -b html -d _build/doctrees   . _build/html
Running Sphinx v7.2.6
WARNING: Invalid configuration value found: 'language = None'. Update your configuration to a valid language code. Falling back to 'en' (English).
WARNING: html_static_path entry '_static' does not exist
loading pickled environment... done
building [mo]: targets for 0 po files that are out of date
writing output... 
building [html]: targets for 0 source files that are out of date
updating environment: 0 added, 1 changed, 0 removed
reading sources... [100%] api/monty_hall_game
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
copying assets... copying static files... done
copying extra files... done
done
writing output... [100%] indexame
generating indices... genindex py-modindex done
highlighting module code... [100%] monty_hall_game.monty_hall_game
writing additional pages... search done
dumping search index in English (code: en)... done
dumping object inventory... done
build succeeded, 2 warnings.

The HTML pages are in _build/html.

Build finished. The HTML pages are in _build/html.

New files (autogenerated with sphinx-quickstart and sphinx-apidoc)#

    docs/
        conf.py              # Sphinx configuration file
        index.rst            # Index page (in markdown format)
        make.bat             # Windows build file
        Makefile             # Linux/MacOS build file
    docs/api
        modules.rst          # Module page
        monty_hall_game.rst  # Module page

Step 4. Add tests#

We will use pytest as testing framework. New files:

monty_hall_game/
    tests
        test_game_module.py

We can run the test suite with

Let’s look at the code in more detail (see files in monty-hall-game4 folder)

%cd ..
!python3 -m pytest tests
/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4
collected 3 items                                                              

tests/test_game_module.py ...                                            [100%]

============================== 3 passed in 0.01s ===============================

Continuous integration with GitHub Actions#

GitHub Actions docs

Quick guide to GitHub action#

create .github/workflows/test.yml (any name.yml will do, you can have several) with steps:

  1. checkout the repo

  2. install Python

  3. install your package and its dependencies

  4. run the tests

# This is a GitHub workflow defining a set of jobs with a set of steps.
# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
#
name: Tests

on:
  pull_request:
  push:

jobs:
  test:
    runs-on: ubuntu-22.04

    strategy:
      matrix:
        python:
          - "3.9"
          - "3.10"

    steps:
      # checkout the repository
      - uses: actions/checkout@v3

      # setup Python
      - name: Install Python ${{ matrix.python }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python }}

      # preserve pip cache to speed up installation
      - name: Cache pip
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          # Look to see if there is a cache hit for the corresponding requirements file
          key: ${{ runner.os }}-pip-${{ hashFiles('*requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install Python dependencies
        run: |
          pip install --upgrade pip
          pip install --upgrade .[dev]
          pip freeze

      - name: Run tests
        run: |
          pytest -v --color=yes

Publishing the documentation#

Similarly, we can use Github actions to publish a webpage hosted by Github. We will first make a stand-alone action that simply uploads the built web-page as an artifact. This can be downloaded an inspected.

ame: Build documentation

on:
  workflow_dispatch:
  workflow_call:
  pull_request:
    branches:
      - main
jobs:
  build_docs:
    runs-on: ubuntu-22.04
    env:
      PYTHON_VERSION: "3.10"

    steps:
      # checkout the repository
      - uses: actions/checkout@v4

      # setup Python
      - name: Install Python ${{ env.PYTHON_VERSION }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      # preserve pip cache to speed up installation
      - name: Cache pip
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          # Look to see if there is a cache hit for the corresponding requirements file
          key: ${{ runner.os }}-pip-${{ hashFiles('*requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-
      - name: Install Python dependencies
        run: |
          python -m pip install --upgrade pip
          python -m pip install --upgrade .[dev]
          python -m pip freeze
      - name: Build documentation
        run: |
          cd docs
          make html
      - name: Upload documentation artifact
        uses: actions/upload-artifact@v3
        with:
          name: documentation
          path: ./docs/_build/html
          if-no-files-found: error

By adding workflow_call to the settings that decide when this is executed, we allow this workflow to be run from inside another workflow. We can then create a Github publishing workflow, that only runs on the main branch, that pushes the web page to Github

name: Github Pages

on:
  push:
    branches: [main]

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment
concurrency:
  group: "pages"
  cancel-in-progress: true

jobs:
  build-docs:
    uses: ./.github/workflows/build_docs.yml

  deploy:
    needs: [build-docs]

    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    runs-on: ubuntu-latest
    steps:
      - name: Download docs artifact
        # docs artifact is uploaded by build-docs job
        uses: actions/download-artifact@v3
        with:
          name: documentation
          path: "./public"

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: "./public"

      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Pages
        uses: actions/configure-pages@v3

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2

Check:

UiO-IN3110/monty_hall_game

Check the build status page to see if your build passes or fails according to the return status of the build command by visiting your GitHub repository and selecting actions.

Current directory structure#

monyty_hall_game/
    README.md                      # Installation instructions
    requirements.txt               # Dependencies
    setup.py                       # Setuptools
    monty_hall_game/               # Main module
        __init__.py
        web.py
        cli.py
        game_exceptions.py
        monty_hall_game.py
        templates/*.html
        static/*.{png,jpg}
    docs/                          # Sphinx documentation (mostly autogenerated)
        conf.py
        index.rst
        Makefile
        modules.rst
        monty_hall_game.rst
    tests                          # tests in pytest format
        test_game_module.py
    .github/workflows/test.yml     # continuous integration
    .gitignore

Deploy your Python library to PyPI#

Wouldn’t it be nice if your users could just type in:

python3 -m pip install monty_hall_game

This is possible by uploading the package to the Python Package Index (PyPI).

  1. Create a source and wheel distribution with the following command:

python -m pip install build
python -m build .
  1. Upload the distribution to pypi (will ask for the pypi credentials) with:

twine upload -r test dist/*

Result see https://test.pypi.org/project/uio-monty-hall-game/

%cd monty-hall-game4
!rm -rf dist
!python3 -m pip install build
!python3 -m build .
[Errno 2] No such file or directory: 'monty-hall-game4'
/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4
/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages/IPython/core/magics/osm.py:393: UserWarning: using bookmarks requires you to install the `pickleshare` library.
  bkms = self.shell.db.get('bookmarks', {})
Requirement already satisfied: build in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (1.0.3)
Requirement already satisfied: packaging>=19.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from build) (23.2)
Requirement already satisfied: tomli>=1.1.0 in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from build) (2.0.1)
Requirement already satisfied: pyproject_hooks in /home/dokken/Documents/src/UiO/UiO-IN3110.github.io/monty-hall/lib/python3.10/site-packages (from build) (1.0.0)
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools)
* Getting build dependencies for sdist...
running egg_info
writing monty_hall_game.egg-info/PKG-INFO
writing dependency_links to monty_hall_game.egg-info/dependency_links.txt
writing entry points to monty_hall_game.egg-info/entry_points.txt
writing requirements to monty_hall_game.egg-info/requires.txt
writing top-level names to monty_hall_game.egg-info/top_level.txt
reading manifest file 'monty_hall_game.egg-info/SOURCES.txt'
writing manifest file 'monty_hall_game.egg-info/SOURCES.txt'
* Building sdist...
running sdist
running egg_info
writing monty_hall_game.egg-info/PKG-INFO
writing dependency_links to monty_hall_game.egg-info/dependency_links.txt
writing entry points to monty_hall_game.egg-info/entry_points.txt
writing requirements to monty_hall_game.egg-info/requires.txt
writing top-level names to monty_hall_game.egg-info/top_level.txt
reading manifest file 'monty_hall_game.egg-info/SOURCES.txt'
writing manifest file 'monty_hall_game.egg-info/SOURCES.txt'
running check
creating monty_hall_game-0.1.0
creating monty_hall_game-0.1.0/monty_hall_game
creating monty_hall_game-0.1.0/monty_hall_game.egg-info
creating monty_hall_game-0.1.0/tests
copying files to monty_hall_game-0.1.0...
copying README.md -> monty_hall_game-0.1.0
copying dev-requirements.txt -> monty_hall_game-0.1.0
copying pyproject.toml -> monty_hall_game-0.1.0
copying requirements.txt -> monty_hall_game-0.1.0
copying monty_hall_game/__init__.py -> monty_hall_game-0.1.0/monty_hall_game
copying monty_hall_game/__main__.py -> monty_hall_game-0.1.0/monty_hall_game
copying monty_hall_game/cli.py -> monty_hall_game-0.1.0/monty_hall_game
copying monty_hall_game/game_exceptions.py -> monty_hall_game-0.1.0/monty_hall_game
copying monty_hall_game/monty_hall_game.py -> monty_hall_game-0.1.0/monty_hall_game
copying monty_hall_game/web.py -> monty_hall_game-0.1.0/monty_hall_game
copying monty_hall_game.egg-info/PKG-INFO -> monty_hall_game-0.1.0/monty_hall_game.egg-info
copying monty_hall_game.egg-info/SOURCES.txt -> monty_hall_game-0.1.0/monty_hall_game.egg-info
copying monty_hall_game.egg-info/dependency_links.txt -> monty_hall_game-0.1.0/monty_hall_game.egg-info
copying monty_hall_game.egg-info/entry_points.txt -> monty_hall_game-0.1.0/monty_hall_game.egg-info
copying monty_hall_game.egg-info/requires.txt -> monty_hall_game-0.1.0/monty_hall_game.egg-info
copying monty_hall_game.egg-info/top_level.txt -> monty_hall_game-0.1.0/monty_hall_game.egg-info
copying tests/test_game_module.py -> monty_hall_game-0.1.0/tests
Writing monty_hall_game-0.1.0/setup.cfg
Creating tar archive
removing 'monty_hall_game-0.1.0' (and everything under it)
* Building wheel from sdist
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools)
* Getting build dependencies for wheel...
running egg_info
writing monty_hall_game.egg-info/PKG-INFO
writing dependency_links to monty_hall_game.egg-info/dependency_links.txt
writing entry points to monty_hall_game.egg-info/entry_points.txt
writing requirements to monty_hall_game.egg-info/requires.txt
writing top-level names to monty_hall_game.egg-info/top_level.txt
reading manifest file 'monty_hall_game.egg-info/SOURCES.txt'
writing manifest file 'monty_hall_game.egg-info/SOURCES.txt'
* Installing packages in isolated environment... (wheel)
* Building wheel...
running bdist_wheel
running build
running build_py
creating build
creating build/lib
creating build/lib/monty_hall_game
copying monty_hall_game/cli.py -> build/lib/monty_hall_game
copying monty_hall_game/monty_hall_game.py -> build/lib/monty_hall_game
copying monty_hall_game/game_exceptions.py -> build/lib/monty_hall_game
copying monty_hall_game/__main__.py -> build/lib/monty_hall_game
copying monty_hall_game/__init__.py -> build/lib/monty_hall_game
copying monty_hall_game/web.py -> build/lib/monty_hall_game
running egg_info
writing monty_hall_game.egg-info/PKG-INFO
writing dependency_links to monty_hall_game.egg-info/dependency_links.txt
writing entry points to monty_hall_game.egg-info/entry_points.txt
writing requirements to monty_hall_game.egg-info/requires.txt
writing top-level names to monty_hall_game.egg-info/top_level.txt
reading manifest file 'monty_hall_game.egg-info/SOURCES.txt'
writing manifest file 'monty_hall_game.egg-info/SOURCES.txt'
installing to build/bdist.linux-x86_64/wheel
running install
running install_lib
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/wheel
creating build/bdist.linux-x86_64/wheel/monty_hall_game
copying build/lib/monty_hall_game/cli.py -> build/bdist.linux-x86_64/wheel/monty_hall_game
copying build/lib/monty_hall_game/monty_hall_game.py -> build/bdist.linux-x86_64/wheel/monty_hall_game
copying build/lib/monty_hall_game/game_exceptions.py -> build/bdist.linux-x86_64/wheel/monty_hall_game
copying build/lib/monty_hall_game/__main__.py -> build/bdist.linux-x86_64/wheel/monty_hall_game
copying build/lib/monty_hall_game/__init__.py -> build/bdist.linux-x86_64/wheel/monty_hall_game
copying build/lib/monty_hall_game/web.py -> build/bdist.linux-x86_64/wheel/monty_hall_game
running install_egg_info
Copying monty_hall_game.egg-info to build/bdist.linux-x86_64/wheel/monty_hall_game-0.1.0-py3.10.egg-info
running install_scripts
creating build/bdist.linux-x86_64/wheel/monty_hall_game-0.1.0.dist-info/WHEEL
creating '/home/dokken/Documents/src/UiO/UiO-IN3110.github.io/lectures/production/from-script-to-project/monty-hall-game4/dist/.tmp-ecy7fq2k/monty_hall_game-0.1.0-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to it
adding 'monty_hall_game/__init__.py'
adding 'monty_hall_game/__main__.py'
adding 'monty_hall_game/cli.py'
adding 'monty_hall_game/game_exceptions.py'
adding 'monty_hall_game/monty_hall_game.py'
adding 'monty_hall_game/web.py'
adding 'monty_hall_game-0.1.0.dist-info/METADATA'
adding 'monty_hall_game-0.1.0.dist-info/WHEEL'
adding 'monty_hall_game-0.1.0.dist-info/entry_points.txt'
adding 'monty_hall_game-0.1.0.dist-info/top_level.txt'
adding 'monty_hall_game-0.1.0.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built monty_hall_game-0.1.0.tar.gz and monty_hall_game-0.1.0-py3-none-any.whl
ls dist
monty_hall_game-0.1.0-py3-none-any.whl  monty_hall_game-0.1.0.tar.gz
!twine upload -r test dist/*
Uploading distributions to https://test.pypi.org/legacy/
Uploading uio_monty_hall_game-2022.11.17-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.2/8.2 kB00:00278.3 kB/s
?25hUploading uio-monty-hall-game-2022.11.17.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.8/6.8 kB00:00?
?25h
View at:
https://test.pypi.org/project/uio-monty-hall-game/2022.11.17/

Final directory layout#

!tree -I __pycache__ .
.
├── build
│   ├── bdist.linux-x86_64
│   └── lib
│       └── monty_hall_game
│           ├── cli.py
│           ├── game_exceptions.py
│           ├── __init__.py
│           ├── __main__.py
│           ├── monty_hall_game.py
│           └── web.py
├── dev-requirements.txt
├── dist
│   ├── monty_hall_game-0.1.0-py3-none-any.whl
│   └── monty_hall_game-0.1.0.tar.gz
├── docs
│   ├── api
│   │   ├── index.rst
│   │   └── monty_hall_game.rst
│   ├── _build
│   │   ├── doctrees
│   │   │   ├── api
│   │   │   │   ├── index.doctree
│   │   │   │   └── monty_hall_game.doctree
│   │   │   ├── environment.pickle
│   │   │   └── index.doctree
│   │   └── html
│   │       ├── api
│   │       │   ├── index.html
│   │       │   └── monty_hall_game.html
│   │       ├── genindex.html
│   │       ├── index.html
│   │       ├── _modules
│   │       │   ├── index.html
│   │       │   └── monty_hall_game
│   │       │       ├── game_exceptions.html
│   │       │       └── monty_hall_game.html
│   │       ├── objects.inv
│   │       ├── py-modindex.html
│   │       ├── search.html
│   │       ├── searchindex.js
│   │       ├── _sources
│   │       │   ├── api
│   │       │   │   ├── index.rst.txt
│   │       │   │   └── monty_hall_game.rst.txt
│   │       │   └── index.rst.txt
│   │       └── _static
│   │           ├── alabaster.css
│   │           ├── basic.css
│   │           ├── custom.css
│   │           ├── doctools.js
│   │           ├── documentation_options.js
│   │           ├── file.png
│   │           ├── language_data.js
│   │           ├── minus.png
│   │           ├── plus.png
│   │           ├── pygments.css
│   │           ├── searchtools.js
│   │           └── sphinx_highlight.js
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   └── Makefile
├── monty_hall_game
│   ├── cli.py
│   ├── game_exceptions.py
│   ├── __init__.py
│   ├── __main__.py
│   ├── monty_hall_game.py
│   ├── static
│   │   ├── car.jpg
│   │   ├── door1.png
│   │   ├── door2.png
│   │   ├── door3.png
│   │   ├── goat.jpg
│   │   ├── goat_lost.jpg
│   │   └── winner.png
│   ├── templates
│   │   ├── final.html
│   │   ├── layout7.html
│   │   ├── reselect.html
│   │   └── select.html
│   └── web.py
├── monty_hall_game.egg-info
│   ├── dependency_links.txt
│   ├── entry_points.txt
│   ├── PKG-INFO
│   ├── requires.txt
│   ├── SOURCES.txt
│   └── top_level.txt
├── pyproject.toml
├── README.md
├── readthedocs.yml
├── requirements.txt
├── runtime.txt
└── tests
    └── test_game_module.py

22 directories, 74 files
monyty_hall_game/
    README.md                      # Installation instructions
    requirements.txt               # Dependencies
    setup.py                       # Setuptools
    monty_hall_game/               # Our package
        __init__.py
        game_exceptions.py
        monty_hall_game.py

    bin/                           # Scripts
        play_monty_hall_cli.py
        play_monty_hall_web.py
        templates/*.html
    docs/                          # Sphinx documentation (mostly autogenerated)
        conf.py
        index.rst
        Makefile
        modules.rst
        monty_hall_game.rst
    tests                          # tests in pytest format
        test_game_module.py
    .github/workflows/test.yml     # continuous integration
    .gitignore

Summary of today’s topics#

  • Installation

    • requirements.txt

    • Package + pyproject.toml

    • README.md

  • Testing

    • Continuous integration (with GitHub)

  • Documentation

    • Sphinx

    • Publishing on Github

  • Deployment / Publishing

    • Deployment of Python packages with PyPI