Skip to content

Commit f7d729b

Browse files
Add nox and tests for pytest (#8)
- Dropped the required Textual version back down to 3.7.1 (last 3.x.x release) to maintain compatibility with Textual 3.x.x. - Made some changes to the demo to make the library compatible with Textual 3.x.x - Added `/tests` directory with unit tests for the ColorOMatic, a [pytest] section in `pyproject.toml`, and added `just test` command to the justfile. - Added Nox testing and `noxfile.py` to run tests in different Python versions and across different versions of Textual. - Added pytest, pytest-asyncio, and pytest-textual-snapshot to dev dependencies. - Deleted `ci-requirements.txt` as it is no longer needed with the new Nox setup. - Changed `ci-checks.yml` to run Nox instead of individual commands for MyPy, Ruff, Pytest, etc.
1 parent b4844cc commit f7d729b

File tree

15 files changed

+823
-105
lines changed

15 files changed

+823
-105
lines changed

.github/workflows/ci-checks.yml

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,13 @@ jobs:
2020
with:
2121
python-version-file: '.python-version'
2222

23-
- name: Install dependencies
24-
run: |
25-
python -m pip install --upgrade pip
26-
pip install -r ci-requirements.txt
27-
28-
- name: Lint code
29-
run: ruff check src
30-
31-
- name: Run type checks
32-
run: |
33-
mypy src
34-
basedpyright src
35-
36-
# add this when tests are ready
37-
# - name: Run tests
38-
# run: pytest
23+
- name: Setup uv
24+
uses: astral-sh/setup-uv@v6
25+
with:
26+
enable-cache: true
27+
28+
- name: Setup Nox
29+
uses: wntrblm/[email protected]
30+
31+
- name: Run Nox sessions
32+
run: nox

.github/workflows/release.yml

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# HOW THIS WORKFLOW WORKS
2-
# This workflow automates creating a GitHub Release and publishing your Python package to PyPI.
2+
# This workflow automates creating a GitHub Release and publishing the Python package to PyPI.
33

44
# --- Manual Steps ---
55
# 1. Edit `pyproject.toml` to set the new version (e.g., `version = "1.2.3"` or `version = "1.2.4-rc1"` for pre-releases).
@@ -8,42 +8,44 @@
88
# 4. Use the justfile: `just release` to trigger the process.
99

1010
# --- Automation ---
11-
# The workflow then automatically:
1211
# - runs .github/scripts/tag_release.py to create a new tag based on the version in `pyproject.toml`.
13-
# - Pushes the new tag to github which triggers this workflow.
12+
# - Pushes the new tag to github which triggers this workflow file.
1413
# - Checks that the tag matches the version in `pyproject.toml`.
1514
# - Builds the sdist and wheel.
16-
# - Publishes the package to PyPI.
17-
# - Creates a new GitHub Release based on the tag with logic for marking pre-releases.
15+
# - Publishes the package to PyPI using trusted publishing.
16+
# - Reads the release notes from your CHANGELOG.md.
17+
# - Creates a new GitHub Release, marking it as a pre-release if necessary.
18+
1819

1920
name: Create Release and Publish to PyPI
2021

2122
on:
2223
push:
2324
tags:
24-
- "v*" # Runs on any tag starting with "v", e.g., v1.2.3
25+
- "v*" # Runs on any tag starting with "v", e.g., v1.2.3 or v1.2.3-rc1
2526
workflow_dispatch:
2627
inputs:
2728
tag_name:
28-
description: 'Tag name to create release for (e.g., v1.2.3)'
29+
description: 'Tag to create release for (e.g., v1.2.3). Must start with "v".'
2930
required: true
3031
type: string
3132

3233
jobs:
3334
build-and-publish:
3435
name: Build and Publish
3536
runs-on: ubuntu-latest
36-
# These permissions are required for the actions below.
3737
permissions:
3838
id-token: write # Required for Trusted Publishing with PyPI (OIDC).
3939
contents: write # Required to create the GitHub Release.
4040

4141
steps:
42+
# If manually triggered, checkout the specific tag. Otherwise, it checks out the tag that triggered the workflow.
43+
# Fetch all history for all tags so the changelog-reader can find previous tags.
4244
- name: Check out code
4345
uses: actions/checkout@v4
4446
with:
45-
# If manually triggered, checkout the specific tag
46-
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref }}
47+
ref: ${{ github.event.inputs.tag_name || github.ref }}
48+
fetch-depth: 0
4749

4850
- name: Set up Python
4951
uses: actions/setup-python@v5
@@ -53,51 +55,45 @@ jobs:
5355
- name: Install required python packages
5456
run: python -m pip install --upgrade build tomli
5557

56-
- name: Set tag name
57-
id: tag
58-
# github.ref_name is the name of the tag that triggered this workflow,
59-
# if it was triggered by a tag push and not manually.
60-
# inputs.tag_name is used when the workflow is manually triggered.
58+
# Use github.ref_name which reliably gives the tag name (e.g., "v1.2.3")
59+
# Create a step output named 'version' that contains the tag name without the 'v'
60+
- name: Verify tag matches pyproject.toml version
6161
run: |
62-
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
63-
TAG_NAME="${{ inputs.tag_name }}"
64-
else
65-
TAG_NAME="${{ github.ref_name }}"
66-
fi
67-
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
68-
echo "Using tag: $TAG_NAME"
69-
70-
- name: Verify tag matches version
71-
run: |
72-
TAG_VERSION=${{ steps.tag.outputs.tag_name }}
62+
TAG_NAME="${{ github.ref_name }}"
7363
PYPROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
74-
if [ "v$PYPROJECT_VERSION" != "$TAG_VERSION" ]; then
75-
echo "Error: Tag $TAG_VERSION does not match pyproject.toml version v$PYPROJECT_VERSION"
64+
65+
if [ "v$PYPROJECT_VERSION" != "$TAG_NAME" ]; then
66+
echo "Error: Tag '$TAG_NAME' does not match pyproject.toml version 'v$PYPROJECT_VERSION'"
7667
exit 1
77-
fi
68+
fi
69+
70+
echo "Tag and pyproject.toml version match: $TAG_NAME"
71+
echo "version=${TAG_NAME#v}" >> $GITHUB_OUTPUT
7872
7973
- name: Build package
8074
run: python -m build
8175

8276
- name: Publish to PyPI
83-
# This action uses Trusted Publishing, which is configured in your PyPI project settings.
84-
# It avoids the need for storing API tokens as secrets.
8577
uses: pypa/gh-action-pypi-publish@release/v1
8678

87-
- name: Extract changelog for release
88-
id: changelog
89-
run: |
90-
VERSION=${{ steps.tag.outputs.tag_name }}
91-
VERSION=${VERSION#v} # Strip leading "v"
92-
awk "/## \[${VERSION//./\\.}\]/,/^## \[/" CHANGELOG.md | head -n -1 > body.md
79+
- name: Get Changelog Entry
80+
id: changelog_reader
81+
uses: mindsers/changelog-reader-action@v2
82+
with:
83+
validation_level: warn
84+
version: ${{ steps.version_check.outputs.version }}
85+
path: ./CHANGELOG.md
9386

9487
- name: Create GitHub Release
95-
uses: softprops/action-gh-release@v2
88+
uses: ncipollo/release-action@v1
9689
with:
97-
tag_name: ${{ steps.tag.outputs.tag_name }}
98-
# The release title will be "Release vX.X.X" or "Pre-release vX.X.X-rc1"
99-
# depending on whether a hyphen is present in the tag name.
100-
release_name: ${{ startsWith(steps.tag.outputs.tag_name, 'v') && contains(steps.tag.outputs.tag_name, '-') && format('Pre-release {0}', steps.tag.outputs.tag_name) || format('Release {0}', steps.tag.outputs.tag_name) }}
101-
# Marks the release as a "pre-release" on GitHub if the tag contains a hyphen (e.g., "-rc1").
102-
prerelease: ${{ contains(steps.tag.outputs.tag_name, '-') }}
103-
body_path: body.md
90+
# Use the tag name that triggered the workflow
91+
tag: ${{ github.ref_name }}
92+
# The release title will be, e.g., "Release v1.2.3"
93+
name: Release ${{ github.ref_name }}
94+
# The body of the release is the changelog entry from the previous step
95+
body: ${{ steps.changelog_reader.outputs.changes }}
96+
# Automatically mark as pre-release if the tag contains a hyphen (e.g., v1.2.3-rc1)
97+
prerelease: ${{ contains(github.ref_name, '-') }}
98+
# This allows the action to update a release if it already exists
99+
allowUpdates: true

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ build/
55
dist/
66
wheels/
77
*.egg-info
8-
*_cache/
8+
9+
# Textual stuff
10+
snapshot_report.html
911

1012
# Virtual environments
1113
.venv
14+
15+
# tooling stuff
16+
*_cache/
17+
.nox/
18+
.crush

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Textual-Color-O-Matic Changelog
22

3+
## [1.0.1] 2025-07-29
4+
5+
- Dropped the required Textual version back down to 3.7.1 (last 3.x.x release) to maintain compatibility with Textual 3.x.x.
6+
- Made some changes to the demo to make the library compatible with Textual 3.x.x
7+
- Added `/tests` directory with unit tests for the ColorOMatic, a [pytest] section in `pyproject.toml`, and added `just test` command to the justfile.
8+
- Added Nox testing and `noxfile.py` to run tests in different Python versions and across different versions of Textual.
9+
- Added pytest, pytest-asyncio, and pytest-textual-snapshot to dev dependencies.
10+
- Deleted `ci-requirements.txt` as it is no longer needed with the new Nox setup.
11+
- Changed `ci-checks.yml` to run Nox instead of individual commands for MyPy, Ruff, Pytest, etc.
12+
313
## [1.0.0] 2025-07-28
414

515
### Usage / API changes

ci-requirements.txt

Lines changed: 0 additions & 7 deletions
This file was deleted.

justfile

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ typecheck:
3535
format:
3636
@uv run black src
3737

38+
test:
39+
@uv run pytest tests -vvv
40+
41+
test-update:
42+
@uv run pytest tests -vvv --snapshot-update
43+
3844
# Runs ruff, mypy, and black
39-
all-checks: lint typecheck format
40-
echo "All pre-commit checks passed. You're good to PR to main."
45+
all-checks: lint typecheck format test
46+
echo "All checks passed."
47+
48+
# Run the Nox testing suite for comprehensive testing
49+
nox:
50+
nox
4151

4252
# Remove build/dist directories and pyc files
4353
clean:
@@ -48,14 +58,18 @@ clean:
4858
clean-caches:
4959
rm -rf .mypy_cache
5060
rm -rf .ruff_cache
61+
rm -rf .nox
5162

5263
# Remove the virtual environment and lock file
5364
del-env:
5465
rm -rf .venv
5566
rm -rf uv.lock
5667

68+
nuke: clean clean-caches del-env
69+
@echo "All build artifacts and caches have been removed."
70+
5771
# Removes all environment and build stuff
58-
reset: clean clean-caches del-env install
72+
reset: nuke install
5973
@echo "Environment reset."
6074

6175
# Release the kraken

noxfile.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Config file for Nox sessions
2+
By Edward Jazzhands - 2025
3+
4+
NOTE ABOUT NOX CONFIG:
5+
If you are doing dev work in some kind of niche environment such as a Docker
6+
container or on a server, you might not have symlinks available to you.
7+
In that case, you can set `nox.options.reuse_existing_virtualenvs = True
8+
9+
Setting `nox.options.reuse_existing_virtualenvs` to True will make Nox
10+
reuse environments between runs, preventing however many GB of data from
11+
being written to your drive every time you run it. (Note: saves environments
12+
between runs of Nox, not between sessions of the same run).
13+
14+
If you do not need to reuse existing virtual environments, you can set
15+
`nox.options.reuse_existing_virtualenvs = False` and `DELETE_VENV_ON_EXIT = True`
16+
to delete the virtual environments after each session. This will ensure that
17+
you do not have any leftover virtual environments taking up space on your drive.
18+
Nox would just delete them when starting a new session anyway.
19+
"""
20+
21+
import nox
22+
import pathlib
23+
import shutil
24+
25+
# PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12"]
26+
PYTHON_VERSIONS = ["3.9"]
27+
MAJOR_TEXTUAL_VERSIONS = [3, 4, 5]
28+
29+
##############
30+
# NOX CONFIG #
31+
##############
32+
33+
nox.options.reuse_existing_virtualenvs = True
34+
nox.options.stop_on_first_error = True
35+
DELETE_VENV_ON_EXIT = False
36+
37+
if nox.options.reuse_existing_virtualenvs and DELETE_VENV_ON_EXIT:
38+
raise ValueError(
39+
"You cannot set both `nox.options.reuse_existing_virtualenvs`"
40+
"and `DELETE_VENV_ON_EXIT` to True (Technically this would not cause "
41+
"an error, but it would be pointless)."
42+
)
43+
44+
################
45+
# NOX SESSIONS #
46+
################
47+
48+
@nox.session(
49+
venv_backend="uv",
50+
python=PYTHON_VERSIONS,
51+
)
52+
@nox.parametrize("ver", MAJOR_TEXTUAL_VERSIONS)
53+
def tests(session: nox.Session, ver: int) -> None:
54+
55+
session.run_install(
56+
"uv",
57+
"sync",
58+
"--quiet",
59+
"--reinstall",
60+
f"--python={session.virtualenv.location}",
61+
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
62+
external=True,
63+
)
64+
65+
session.run_install(
66+
"uv", "pip", "install",
67+
f"textual<{ver + 1}.0.0",
68+
external=True,
69+
)
70+
session.run("uv", "pip", "show", "textual")
71+
# EXPLANATION: The `ver + 1` is a trick to make UV
72+
# only download the last version of each major revision of Textual.
73+
# If the current version is 3, we're saying `install textual<4.0.0`.
74+
# This will make UV grab the highest version of Textual 3.x.x, which is 3.7.1.
75+
# The last `uv pip show textual` is just for logging purposes.
76+
77+
# These are all assuming you have corresponding
78+
# sections in your pyproject.toml for configuring each tool:
79+
session.run("ruff", "check", "src")
80+
session.run("mypy", "src")
81+
session.run("basedpyright", "src")
82+
session.run("pytest", "tests", "-vvv")
83+
84+
# This code here will make Nox delete each session after it finishes.
85+
# This might be preferable to allowing it all to accumulate and then deleting
86+
# the folder afterwards (for example if testing would use dozens of GB of data and
87+
# you don't have the disk space to store it all temporarily).
88+
session_path = pathlib.Path(session.virtualenv.location)
89+
if session_path.exists() and session_path.is_dir() and DELETE_VENV_ON_EXIT:
90+
shutil.rmtree(session_path)

0 commit comments

Comments
 (0)