Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Publish to PyPI

on:
release:
types: [published]

jobs:
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
permissions:
id-token: write # Required for trusted publishing
contents: read

steps:
- uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Build package
run: uv build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
21 changes: 16 additions & 5 deletions .github/workflows/run_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,27 @@ jobs:
fail-fast: true
matrix:
python-version:
- "3.x"
- "pypy-3.6"
- "pypy-3.7"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: actions/setup-python@v2
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install deps
run: uv sync --all-groups

- name: Run tests
run: make test
50 changes: 29 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,50 +1,58 @@
.RECIPEPREFIX = >

# Lint, format, test
.PHONY: all
all: clean check format test

# Remove build artefacts
.PHONY: clean
clean:
> @echo Cleaning
> @rm -rf build

# Rebuild the pickled `.dat` file from `rmsfact.txt`
.PHONY: build_binary_data
build_binary_data:
> @echo Building binary data
> @rm -f rmsfact/data/rmsfact.dat
> @python rmsfact/data/build_rmsfact.py
> @rm -rf build dist

# Build the package in wheel and source form
.PHONY: build
build: clean build_binary_data
build: clean
> @echo Building package
> @python -m build
> @uv build

# Build the package and install locally for development
# Install dependencies and set up development environment
.PHONY: install_dev
install_dev: build
> @echo Installing locally
> @python -m pip install -e .
install_dev:
> @echo Setting up development environment
> @uv sync

# Install
# Install the package
.PHONY: install
install: build
> @echo Installing
> @python -m pip install .
> @echo Installing package
> @uv pip install dist/*.whl

# Build the package and upload to TestPyPI
.PHONY: upload_test
upload_test: test build
> @echo Uploading to testpypi
> @python -m twine upload --repository testpypi dist/*
> @uv run twine upload --repository testpypi dist/*

# Build the package and upload to PyPI
.PHONY: upload
upload: test build
> @echo Uploading to PyPi
> @python -m twine upload dist/*
> @uv run twine upload dist/*

# Run unit tests
.PHONY: test
test: clean build_binary_data
test: clean
> @echo Running tests
> @python test
> @uv run pytest

# Format code
.PHONY: format
format:
> @uv run ruff check --select I --fix
> @uv run ruff format

# Lint
.PHONY: check
check:
> @uv run ruff check
87 changes: 64 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ site stallmanfacts.com (which is accessible only via <http://archive.org>).

# Installation

``` python
python -m pip install rmsfact
```bash
uv pip install rmsfact
```

Or with pip:

```bash
pip install rmsfact
```


Expand All @@ -30,48 +36,83 @@ You can also run `python -m rmsfact` from a shell.

# Building from source

In the event you want to build the package from source, clone the repository...
This project uses [uv](https://github.com/astral-sh/uv) for dependency management and building.

```
Clone the repository:

```bash
git clone https://github.com/lewinfox/rmsfact.git
cd rmsfact
```

... `cd` into the project directory...
Install uv if you don't have it:

```
cd rmsfact
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

...and install with `make`
Set up the development environment:

```
make install
```bash
make install_dev
```

This builds the package and then runs `python -m pip install .`
This will create a virtual environment, install dependencies, and set up the package for development.


# Making changes

To add a new fact you can edit `rmsfact/data/rmsfact.txt`.
To add a new fact you can edit `rmsfact/data/rmsfact.txt`. The text file is read directly when the package is imported, so no build step is needed.

```
```bash
echo "A new fact" >> rmsfact/data/rmsfact.txt
```

The `Makefile` provides a couple of useful targets, one of which is `make build_binary_data` which
executes the script `rmsfact/data/build_rms_fact.py` to convert `rmsfact/data/rmsfact.txt` into a
`.dat` file using `pickle`. This `.dat` file is then read when the package is loaded.
The `Makefile` provides several useful targets:

Executing `make build` will rebuild the data file and produce the source and binary packages under
a `dist/` directory. You may need to `python -m pip install build` first.
* `make install_dev`: Set up development environment with uv
* `make test`: Run unit tests with pytest
* `make build`: Build the package (creates source and wheel distributions)
* `make clean`: Remove build artifacts
* `make format`: Format code with ruff
* `make check`: Run linter checks

```
You can build the package with:

```bash
make build
```

## Other makefile targets

* `make install_dev`: Install with `pip -e`
* `make test`: Run unit tests
* `make clean`: Remove build artifacts
# Releasing to PyPI

This project uses GitHub Actions to automatically publish to PyPI when you create a new GitHub release.

## First-time setup

You need to configure PyPI's Trusted Publishers feature (one-time setup):

1. Go to [PyPI](https://pypi.org/) and log in to your account
2. Navigate to your project's settings (or create the project first if it doesn't exist)
3. Go to the "Publishing" section
4. Add a new publisher with these settings:
- **PyPI Project Name**: `rmsfact`
- **Owner**: `lewinfox` (your GitHub username)
- **Repository name**: `rmsfact`
- **Workflow name**: `publish.yml`
- **Environment name**: (leave blank)

## Creating a release

Once trusted publishing is configured, simply create a new GitHub release:

1. Update the version in [pyproject.toml](pyproject.toml:7)
2. Commit and push your changes
3. Create a new release on GitHub with a tag matching the version (e.g., `v0.5.0`)
4. The GitHub Action will automatically build and publish to PyPI

You can also manually publish with:

```bash
make upload
```
37 changes: 33 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "rmsfact"
version = "0.5.0"
description = "Display a randomly selected quote about Richard M. Stallman."
readme = "README.md"
authors = [{ name = "Lewin Appleton-Fox", email = "lewin.a.f@gmail.com" }]
requires-python = ">=3.6"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
]

[project.urls]
Homepage = "https://www.github.com/lewinfox/rmsfact"
Issues = "https://www.github.com/lewinfox/rmsfact/issues"

[project.optional-dependencies]
dev = ["pytest", "twine"]

[tool.hatch.build.targets.wheel]
packages = ["rmsfact"]

[tool.hatch.build.targets.sdist]
include = ["/rmsfact"]

[dependency-groups]
dev = [
"pytest>=7.0.1",
"ruff>=0.0.17",
]
build-backend = "setuptools.build_meta"
27 changes: 0 additions & 27 deletions rmsfact/data/build_rmsfact.py

This file was deleted.

21 changes: 9 additions & 12 deletions rmsfact/new_rmsfact.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import os
import pickle
import random
from typing import Callable


# Rather than having the `rmsfact()` function parse the source file each time, using a closure like
# this allows us to offload the parsing and data validation (not that there is any at the moment)
# and save time when `rmsfact()` is called.
#
# TODO: Is there an advantage to using `pickle` or similar to store a binary version of the `facts`
# object rather than parsing the text file every time the package is loaded?


def _new_rmsfact() -> Callable:
"""
Generate an `rmsfact()` function

This function runs when the package is imported. It loads the "facts" from their binary store
This function runs when the package is imported. It loads the "facts" from the text file
and returns a function that will retrieve a random fact.

Returns
Expand All @@ -31,13 +26,15 @@ def _new_rmsfact() -> Callable:
>>> f = _new_rmsfact()
>>> fact = f()
"""
# TODO: Is there a more Pythonic way of referring to the file?
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.normpath("{ROOT_DIR}/data".format(ROOT_DIR=ROOT_DIR))
FACT_FILE = os.path.normpath("{DATA_DIR}/rmsfact.dat".format(DATA_DIR=DATA_DIR))
# TODO: Error handling needed here?
with open(FACT_FILE, "rb") as f:
facts = pickle.load(f)
FACT_FILE = os.path.normpath("{DATA_DIR}/rmsfact.txt".format(DATA_DIR=DATA_DIR))

with open(FACT_FILE, "r") as f:
lines = f.readlines()
facts = [
line.strip() for line in lines if line.strip() and not line.startswith("#")
]

n_facts = len(facts)

Expand All @@ -51,7 +48,7 @@ def rmsfact() -> str:
str
A randomly-selected fact.
"""
idx = random.randint(0, n_facts)
idx = random.randint(0, n_facts - 1)
return facts[idx]

return rmsfact
Loading