Skip to content

miraisolutions/secretsanta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

secretsanta

This repository implements a basic Python version of a Secret Santa utility. It is meant to serve as a tutorial for beginners interested in Python package development. Each section below mentions typical tools and utilities in a natural order of developing Python packages.

PyPI - Python Version PyPI PyPI - Downloads PyPI - License

Build Status

codecov

Table of Contents

  1. Development
    a. Virtual environments
    b. Project requirements & Environment Setup
  2. Testing
    a. Running Tests with Nox
    b. PyCharm file types
    c. Type hints d. Property testing
    e. Mocks in unit tests
  3. Documentation
    a. Building Docs with Nox
  4. Usage
    a. Jupyter notebook
    b. Command-line interface
    c. Package installation & CLI
  5. Continuous integration
  6. Miscellaneous

Development

We assume PyCharm on Ubuntu >= 20.04 as the development environment, but you might as well use a newer Linux version or even Windows instead.

In PyCharm, check out this repository into a new project, e.g. under

VCS > Checkout from Version Control

Shell commands below should be entered in the Terminal pane of PyCharm.

There is no shortcut in PyCharm to send code from the editor to the terminal, so you need to copy-paste commands instead.

Project requirements & Environment Setup

This project uses uv for dependency management and Nox for task automation and testing across multiple Python versions.

Important: make sure all commands are executed inside the virtual environment, e.g. at such a prompt:

#> (venv) localuser@Ubuntu:~/PyCharm/secretsanta$

First, ensure you have uv installed. You can install them into your global Python environment or use pipx:

pip install uv
# or
pipx install uv

See also other installation options

Check that uv has been installed, and its version:

uv --version

To set up your development environment, synchronize it with the locked dependencies specified in uv.lock:

# Install runtime, dev, test, and docs dependencies
uv sync --dev

You can add dependencies with uv add some_package, optionally with a version specifier (e.g. uv add some_package>=1.2.3). This will modify pyproject.toml and uv.lock and re-sync the environment.

If you modify dependencies in pyproject.toml, update the lock file separately:

uv lock

Then re-sync your environment:

uv sync --dev

You can also run commands within the managed environment using uv run:

uv run -- python secretsanta/cli/cli.py --help

Virtual environments

A virtual environment for the project is created automatically by uv sync. This keeps the global Python environment clean. A couple of useful references about virtual environments if you've never used them before:

Configure the PyCharm project with the project's Python virtual environment under

File > Settings > Project: secretsanta > Python Interpreter

Click on Add Interpreter and select Add Local Interpreter, then choose Select Existing, using <PROJECT_PATH>/.venv/bin/python as the path.

We do not use pipenv here. You may however use it to create a new environment in a similar way.

With these settings, anything you execute within the PyCharm project, either at the Terminal or in the Python Console, will run in the virtual environment. Close and re-open PyCharm to make sure the settings are picked up.

Note that you can still temporarily leave the virtual environment from an active Terminal using

deactivate

and re-activate it using

source ./venv/bin/activate

You can also switch to a different project interpreter in PyCharm (Ctrl + Shift + A, search for Switch Project Interpreter). Open terminals and Python consoles then need to be restarted for the environment to match the project interpreter.

Testing

There are multiple ways to define and execute tests. Two of the most common ones are doctest and unittest.

The doctest module allows to run code examples / tests that are defined as part of docstrings.

Use the following command to see this in action. The -v flag allows us to see verbose output. In case everything is fine, we would not see any output otherwise.

uv run python -m doctest secretsanta/main/core.py -v
# Or run via nox (included in the 'tests' session)
uv run nox -s tests -- -m doctest secretsanta/main/core.py -v

It is possible to run code style checks with ruff:

# Run directly
uv run ruff check
uv run ruff format --check
# Or run via nox
uv run nox -s lint

If all is fine, you will not see any output from ruff directly. nox will report success.

Unit tests are kept under tests.

Running Tests with Nox

Nox is used to automate testing across multiple Python versions (defined in noxfile.py).

List available Nox sessions:

uv run nox --list

Run all test sessions (for Python 3.9, 3.10, 3.11, 3.12):

uv run nox -s tests

Run tests for a specific Python version:

uv run nox -s tests-3.10

Run linting session:

uv run nox -s lint

Run all sessions marked as default:

uv run nox

Nox handles creating temporary virtual environments for each session, installing dependencies using uv, and running the specified commands. Test coverage is measured using pytest-cov (see .coveragerc and pyproject.toml for configuration).

PyCharm file types

In PyCharm, you can associate files to a certain type under:

File > Settings > Editor > File Types

E.g. use this to get .coveragerc marked up as INI (you can do this after installing the .ini support PyCharm plugin). Alternatively, you can register the *.ini and .coveragerc patterns to the existing Buildout Config file type.

Type hints

Type hints define what type function arguments and return values should be. They are both a source of documentation and testing framework to identify bugs more easily, see also PEP 484.

mypy comes installed via uv sync --dev.

Run something like below:

uv run mypy ./secretsanta/main/core.py
uv run mypy ./tests
uv run mypy .
# Or run via nox (if a session is added)
# nox -s typecheck

to test if the type hints of .py file(s) are correct (in which case it would typically output a "Success" message).

Property testing

We use Hypothesis to define a property test for our matching function: generated example inputs are tested against desired properties. Hypothesis' generator can be configured to produce typical data structures, filled with various instances of primitive types. This is done by composing specific annotations.

  • The decorator @given(...) must be present before the test function that shall use generated input.
  • Generated arguments are defined in a comma-separated list, and will be passed to the test function in order:
from hypothesis import given
from hypothesis.strategies import text, integers


@given(text(), integers())
def test_some_thing(a_string, an_int):
    return
  • Generation can be controlled by various optional parameters, e.g. text(min_size=2) for testing with strings that have at least 2 characters.

Mocks in unit tests

Mock objects are used to avoid external side effects. We use the standard Python package unittest.mock. This provides a @patch decorator, which allows us to specify classes to be mocked within the scope of a given test case. See test_funs.py and test_core.py for examples.

Documentation

Documentation is done using Sphinx. We use Google style docstrings as that seems to be prevalent in the industry, with the addition of napoleon Sphinx extension.

The required dependencies, defined in pyproject.toml (e.g. Sphinx) are installed via uv sync --dev.

Initializing documentation - already done - for reference

sphinx-quickstart

This will lead through an interactive generation process.

Suggested values / options are listed here. Hitting enter without typing anything will take the suggested default shown inside square brackets [ ].

  • Root path for the documentation [.]: docs
  • Separate source and build directories (y/n) [n]: y
  • Name prefix for templates and static dir[_]: Enter
  • Project name: secretsanta
  • Author name(s): Mirai Solutions
  • Project version[]: 0.1
  • Project release[0.1]: 0.1.1
  • Project language [en]: None
  • Source file suffix [.rst]: .rst
  • Name of your master document (without suffix) [index]: Enter
  • Do you want to use epub builder (y/n) [n]: n
  • autodoc: automatically insert docstrings from modules (y/n) [n]: y
  • doctest: automatically test code snippets in doctest blocks (y/n) [n]: y
  • intersphinx: link between Sphinx documentation of different projects (y/n) [n]: y
  • todo: write "todo" entries that can be shown or hidden on build (y/n) [n]: n
  • coverage: checks for documentation coverage (y/n) [n]: y
  • imgmath: include math, rendered as PNG or SVG images (y/n) [n]: n
  • mathjax: include math, rendered in the browser by MathJax (y/n) [n]: y
  • ifconfig: conditional inclusion of content based on config values (y/n) [n]: n
  • viewcode: include links to the source code of documented Python objects (y/n) [n]: y
  • githubpages: create .nojekyll file to publish the document on GitHub pages (y/n) [n]: n
  • Create Makefile? (y/n) [y]: y
  • Create Windows command file? (y/n) [y]: n

In order to use autodoc, one needs to uncomment the corresponding line in docs/source/conf.py:

sys.path.insert(0, os.path.abspath(...

And set the appropriate path to the directory containing the modules to be documented.

For Sphinx/autodoc to work, the docstrings must be written in correct reStructuredText, see documentation for details.

Building Docs with Nox

Use Nox to build the documentation:

uv run nox -s docs

This command runs sphinx-build in a dedicated environment managed by Nox.

You can view the documentation by opening docs/build/html/index.html in your browser.

Previewing the .rst files directly in PyCharm might not render Sphinx directives correctly.

Usage

Jupyter Notebook

The Jupyter notebook SecretSanta.ipynb illustrates the usage of the secretsanta package.

It can be run in your browser (or directly in PyCharm if you have the professional edition):

jupyter notebook SecretSanta.ipynb

Below gives you some useful information about the location of Jupyter related directories, e.g. configuration:

jupyter --path

Additionally, you can open and run SecretSanta.ipynb in vs code, provided:

  • you have the Jupyter extension installed
  • you add the jupyter dependencies to your development environment: uv sync --all-groups

A few additional links to some typical early Jupyter topics:

Command-line Interface (CLI)

Python's ecosystem offers several ways to tackle command-line interfaces. The traditional standard method is to use the argparse module that is part of the standard library. This can be complemented by something like argparsetree for larger and more complex command-line applications.

Here we have chosen to use Click instead, which allows us to define our CLI via decorated functions in a neat and compact way. Other potential alternatives could be docopt or Invoke.

A nice comparison is available here.

In order to run the CLI commands during development, use uv run:

uv run -- santa --help
uv run -- santa makedict --help
uv run -- santa makedict "./validation/participants.json"

Alternatively, activate your virtual environment (where dependencies are installed via uv sync) and run directly:

# Assuming your venv is activated
santa --help
santa makedict "./validation/participants.json"

Package Installation & CLI

If you install the package, you can use the CLI tool as designed for the end user:

Build the package wheel

uv build --wheel # creates build and dist directories

Install in a new project / environment

On Windows
uv init # creates a new uv project
uv add ..\secretsanta\dist\secretsanta-0.1.0-py3-none-any.whl
# if already installed, delete the old uv.lock first
rm uv.lock
uv add ..\secretsanta\dist\secretsanta-0.1.0-py3-none-any.whl
On Ubuntu
uv init # creates a new uv project
uv add ../secretsanta/dist/secretsanta-0.1.0-py3-none-any.whl
# if already installed, delete the old uv.lock first
rm uv.lock
uv add --force-reinstall ./dist/secretsanta-0.1.0.tar.gz

Use the CLI tool

uv run santa --help
uv run santa makedict --help
uv run santa makedict "./validation/participants.json"

Continuous Integration

Continuous Integration (CI) aims to keep state updated to always match the code currently checked in a repository. This typically includes a build, automated test runs, and possibly making sure that the newly built artifacts are deployed to a target environment. This helps developers and users by providing timely feedback and showing what the results of certain checks were on a given version of the code.

We use GitHub Actions to implement CI. Building and checking the package is implemented in python-package.yml. This includes running tests and code linting / formatting checks.

Coverage information is generated and uploaded to codecov, which generates a report out of it.

Build status and coverage reports are linked via badges at the top of this README.

Code scanning for security is performed using CodeQL (codeql.yml).

Dependency updates are managed by Dependabot (see dependabot.yml).

Codecov is configured in codecov.yml, defining the coverage value range (in percent) to match to a color scale, as well as the coverage checks to be performed and their success criteria. See codecov's general configuration and commit status evaluation documentation for more information.

Notifications from codecov can only be delivered via unencrypted webhook URLs. In order to avoid exposing such hooks in a public repository, we do not use this functionality here.

Miscellaneous

  • MANIFEST.in specifies extra files that shall be included in a source distribution.
  • Badges: This README features various badges (at the beginning), including a build status badge and a code coverage status badge.

Logging

The logging package is used to track events after running the project. The main logged events (levels) in Secret Santa are: errors, warnings, and participants info. A log level is set as an environment variable, e.g.:

os.environ["level"] = "ERROR"

All logs activities are collected into a log file that is initiated at the beginning of the code:

logging.basicConfig(filename = path_to_file, level = level, format = '%(asctime)s %(levelname)s %(message)s',
                               datefmt = '%Y/%m/%d %I:%M:%S %p')

A logger is then set:

logger = logging.getLogger(__name__)

All functions used afterwards refer to this logger:

logger.error("Error message")
logger.warning("Warning message")
logger.info("Info")

The log file is automatically created in the log_files directory and can be inspected after the project run is complete.