Merge branch 'python-version-support'

Add Python 3.8-3.13 multi-version support: pytest smoke-test suite,
uv+nox version matrix, importlib_resources backport for 3.8, declared
the previously-undeclared selenium dependency, Gitea Actions CI matrix,
and supporting docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathew Sir Guest the best 2026-05-31 00:05:14 -06:00
commit a2d4b334d7
11 changed files with 1543 additions and 10 deletions

21
.gitea/workflows/test.yml Normal file

@ -0,0 +1,21 @@
name: tests
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Run smoke tests
run: uv run --python ${{ matrix.python-version }} --with . --with pytest pytest -v

1
.gitignore vendored

@ -7,3 +7,4 @@ idea
.env .env
.claude/settings.local.json .claude/settings.local.json
.claude/worktrees/ .claude/worktrees/
uv.lock

@ -24,13 +24,19 @@ flake8 smileyface/
# Run pre-commit hooks manually # Run pre-commit hooks manually
pre-commit run --all-files pre-commit run --all-files
# Run the smoke-test suite on the current interpreter
uv run --with . --with pytest pytest -v
# Run the full Python version matrix (3.8-3.13)
uv run --with nox nox -s tests
``` ```
There is no test suite. Smoke tests live in `tests/` (import, CLI `--help`, and settings checks) and run across the supported Python 3.8-3.13 matrix via `nox`.
## Code Style ## Code Style
- **Black** formatter: 120 char line length, target Python 3.13 - **Black** formatter: 120 char line length, targets Python 3.83.13
- **isort**: profile black, multi_line_output=3, trailing commas, force_grid_wrap=3 - **isort**: profile black, multi_line_output=3, trailing commas, force_grid_wrap=3
- **flake8**: 120 char max, ignores E121/E123/E126/E226/E24/E704/W605 - **flake8**: 120 char max, ignores E121/E123/E126/E226/E24/E704/W605

@ -15,6 +15,16 @@ Activate your desired python environment, then:
poetry install poetry install
Supported Python Versions
==========================
SmileyFace is tested against CPython 3.8 3.13. The supported range is
enforced by a smoke-test matrix.
To run the matrix locally (requires [uv](https://docs.astral.sh/uv/)):
uv python install 3.8 3.9 3.10 3.11 3.12 3.13
uv run --with nox nox -s tests
Usage Usage
====== ======

15
noxfile.py Normal file

@ -0,0 +1,15 @@
import nox
nox.options.default_venv_backend = "uv"
nox.options.reuse_existing_virtualenvs = False
# Newest first. 3.8 is a stretch goal (see the importlib_resources backport).
PYTHON_VERSIONS = ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8"]
@nox.session(python=PYTHON_VERSIONS)
def tests(session):
"""Install the project + pytest and run the smoke suite on each interpreter."""
session.install(".")
session.install("pytest")
session.run("pytest", "-v")

1397
poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -11,6 +11,14 @@ homepage = "https://zavage-software.com/portfolio/smileyface"
repository = "https://git-mirror.zavage.net/zavage-software/smileyface" repository = "https://git-mirror.zavage.net/zavage-software/smileyface"
documentation = "https://git-mirror.zavage.net/zavage-software/smileyface" documentation = "https://git-mirror.zavage.net/zavage-software/smileyface"
keywords = ["cas"] keywords = ["cas"]
classifiers = [
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
packages = [{ include = "smileyface" }] packages = [{ include = "smileyface" }]
include = [ include = [
@ -21,17 +29,21 @@ include = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.13" python = ">=3.8,<4.0"
pydantic-settings = ">=2.0" pydantic-settings = ">=2.0"
click = ">=8.0" click = ">=8.0"
platformdirs = ">=3.0" platformdirs = ">=3.0"
sqlparse = "*" sqlparse = "*"
selenium = ">=4.0"
importlib-resources = { version = ">=5.0", python = "<3.9" }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "*" black = "*"
pre-commit = "*" pre-commit = "*"
isort = "*" isort = "*"
flake8 = "*" flake8 = "*"
pytest = "*"
nox = "*"
#Sphinx = "^5.3.0" #Sphinx = "^5.3.0"
#sphinx-rtd-theme = "^1.3.0" #sphinx-rtd-theme = "^1.3.0"
@ -41,7 +53,7 @@ build-backend = "poetry.core.masonry.api"
[tool.black] [tool.black]
line-length = 120 line-length = 120
target-version = ['py313'] target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313']
[tool.isort] [tool.isort]
multi_line_output = 3 multi_line_output = 3
@ -49,3 +61,7 @@ combine_as_imports = true
include_trailing_comma = true include_trailing_comma = true
force_grid_wrap = 3 force_grid_wrap = 3
ensure_newline_before_comments = true ensure_newline_before_comments = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"

@ -1,7 +1,10 @@
import datetime import datetime
import os import os
from importlib.resources import files try:
from importlib.resources import files # Python 3.9+
except ImportError: # Python 3.8
from importlib_resources import files
import sqlparse import sqlparse

24
tests/test_cli.py Normal file

@ -0,0 +1,24 @@
from click.testing import CliRunner
from smileyface.cli import cli
def test_root_help():
result = CliRunner().invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
assert "SmileyFace" in result.output
def test_every_command_help():
runner = CliRunner()
for name in cli.commands:
result = runner.invoke(cli, [name, "--help"])
assert result.exit_code == 0, f"`{name} --help` failed:\n{result.output}"
def test_scrape_subcommands_help():
runner = CliRunner()
scrape_group = cli.commands["scrape"]
for name in scrape_group.commands:
result = runner.invoke(cli, ["scrape", name, "--help"])
assert result.exit_code == 0, f"`scrape {name} --help` failed:\n{result.output}"

34
tests/test_imports.py Normal file

@ -0,0 +1,34 @@
import importlib
import pkgutil
import smileyface
def test_top_level_package_imports():
importlib.import_module("smileyface")
def test_all_submodules_import():
errors = []
def _on_walk_error(name):
errors.append(f"{name}: failed during package walk")
module_names = [
info.name
for info in pkgutil.walk_packages(
smileyface.__path__,
prefix="smileyface.",
onerror=_on_walk_error,
)
]
assert module_names, "walk_packages found no submodules - check smileyface.__path__"
for name in module_names:
try:
importlib.import_module(name)
except Exception as exc: # noqa: BLE001 - we want every failure, not the first
errors.append(f"{name}: {exc!r}")
assert not errors, "Modules failed to import:\n" + "\n".join(errors)

16
tests/test_settings.py Normal file

@ -0,0 +1,16 @@
from smileyface.settings import AppSettings
def test_defaults():
settings = AppSettings(_env_file=None)
assert settings.sqlite_filename == "smiles.db"
assert settings.skip_validate is False
assert settings.project_dir == ""
def test_env_overrides(monkeypatch):
monkeypatch.setenv("SMILEYFACE_PROJECT_DIR", "/srv/ut4")
monkeypatch.setenv("SMILEYFACE_SKIP_VALIDATE", "true")
settings = AppSettings(_env_file=None)
assert settings.project_dir == "/srv/ut4"
assert settings.skip_validate is True