Putting Your Trash Code in a Garbage Bag

A package on an image. It's a beautiful thing. First you are packaging up the code and then you are packaging up the dependencies. I once saw this meme where there was two images. On one side was a bunch of trash. And on the other side was trash placed in garbage bags. The caption was my code pre and post object oriented programming.

Anyways—

Let's start by creating a new poetry project:

poetry new pack

This will create the following directory structure:

pack/
├── pack/
│   └── __init__.py
├── tests/
│   └── __init__.py
└── pyproject.toml

Creating Your Package

Since we are really original people, we are going to create a package that says "hello world". Add this to the __init__.py under the pack directory:

def greet(name):
    return f"Hello, {name}!"

And let's add a test in pytest while we are at it (under test_greet.py in the tests folder):

from my_package import greet

def test_greet():
    assert greet("World") == "Hello, World!"

Then we need to add pytest to our dependencies:

poetry add pytest

Afterward your pyproject.toml should look like this:

[tool.poetry]
name = "pack"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
pytest = "^8.3.3"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Add an Entry Point

An entry point defines what will run when you run the package directly. Like if you do python run.py and run.py is simply filled with functions, you would need an entry point to allow those functions to run.

Let's add one for our package. First put this in the __init__.py for the package:

def greet(name):
    return f"Hello, {name}!"

def main():
    # Example usage of the greet function
    name = "World"  # You can modify this to take user input if desired
    print(greet(name))

if __name__ == "__main__":
    main()

Now main will run when the package runs. Next add this to the bottom of the pyproject.toml:

[tool.poetry.scripts]
pack-cli = "pack:main"  # This defines the CLI command

Install dependencies and run pytest locally.

poetry install
poetry run pytest

Building the Docker Image

Ok now we have our trash code and we need to put it in its trash bags:

# Use the official Python image from the Docker Hub
FROM python:3.12-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Set the working directory inside the container
WORKDIR /app

# Copy poetry.lock and pyproject.toml files first for better caching of dependencies
COPY pyproject.toml poetry.lock* ./

# Install Poetry and dependencies
RUN pip install --no-cache-dir poetry && poetry install --no-dev

# Copy the rest of the application code, including tests
COPY . .

# Run tests using pytest (optional)
RUN poetry run pytest -v

# Command to run your application using the entry point defined in pyproject.toml
CMD ["poetry", "run", "pack-cli"]  # This runs your CLI command defined in pyproject.toml

This pretty much does what we saw with the flask app. It installs the dependencies we need. Copies over the directory content. But now we run pytest as part of the build step and run our package as our CMD.

Let's build and test it!

docker build -t pack .
docker run pack