The project carried three sets of packaging metadata — pyproject.toml [project], setup.cfg [options] and setup.py kwargs — declaring different dependencies. pyproject.toml wins under any modern backend, so the wheel was correct, but the other two said "Pillow, numpy" and reading either gave the wrong answer about what the library needs. Reduce setup.cfg to its [flake8] section and setup.py to a setup() shim, each pointing at pyproject.toml. Correct the authoritative list while consolidating: - flask was a runtime dependency but is imported only by the fixture HTTP server in tests. Every user was installing Flask, Jinja2, Werkzeug, click, itsdangerous and blinker for nothing. - ebooklib was a runtime dependency and is never imported by the library; epub_reader.py uses zipfile + xml.etree directly. Only the tests use it, to build EPUB fixtures. - requests was declared required but concrete/image.py imports it lazily and degrades gracefully when absent, so it belongs in an extra. - requires-python said >=3.6, which cannot be true: the package uses dataclasses and `from __future__ import annotations`, both 3.7+, and CI tests 3.10/3.12/3.13. Runtime install drops from 7 direct dependencies to 4. Adds test, remote-images and dev extras, and a CI step that installs into an empty venv with only the runtime deps and imports every subpackage — this class of defect is only caught by installing what you ship. Verified: runtime-only install imports all subpackages; `.[test]` runs the full suite, 853 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
7.4 KiB
YAML
192 lines
7.4 KiB
YAML
name: Python CI
|
|
|
|
on:
|
|
push:
|
|
branches: [ main, master, develop ]
|
|
paths-ignore:
|
|
- 'coverage*.svg'
|
|
- 'README.md'
|
|
pull_request:
|
|
branches: [ main, master, develop ]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: self-hosted
|
|
strategy:
|
|
matrix:
|
|
python-version: ['3.10', '3.12', '3.13']
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v4
|
|
with:
|
|
python-version: ${{ matrix.python-version }}
|
|
|
|
- name: Verify declared dependencies are sufficient
|
|
run: |
|
|
# A clean venv with ONLY the declared runtime deps. If an import here
|
|
# fails, install_requires is incomplete and a real `pip install
|
|
# pyWebLayout` would fail the same way for a user.
|
|
python -m venv /tmp/clean-install
|
|
/tmp/clean-install/bin/pip install --upgrade pip
|
|
/tmp/clean-install/bin/pip install .
|
|
/tmp/clean-install/bin/python -c "
|
|
import pyWebLayout.concrete, pyWebLayout.abstract
|
|
import pyWebLayout.io.readers.epub_reader
|
|
import pyWebLayout.io.readers.html_extraction
|
|
import pyWebLayout.layout.ereader_manager
|
|
print('clean install imports OK')
|
|
"
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
# Install package in development mode, with the declared dev extra.
|
|
# Test dependencies belong in setup.cfg, not in an ad-hoc pip line.
|
|
pip install -e '.[dev]'
|
|
|
|
- name: Download initial failed badges
|
|
run: |
|
|
echo "Downloading initial failed badges..."
|
|
|
|
# Create cov_info directory first
|
|
mkdir -p cov_info
|
|
|
|
# Download failed badges as defaults
|
|
curl -o cov_info/coverage.svg "https://img.shields.io/badge/coverage-failed-red.svg"
|
|
curl -o cov_info/coverage-docs.svg "https://img.shields.io/badge/docs-failed-red.svg"
|
|
|
|
echo "Initial failed badges created:"
|
|
ls -la cov_info/coverage*.svg
|
|
|
|
- name: Run tests with pytest
|
|
id: pytest
|
|
continue-on-error: true
|
|
run: |
|
|
# Run tests with coverage
|
|
python -m pytest tests/ -v --cov=pyWebLayout --cov-report=term-missing --cov-report=json --cov-report=html --cov-report=xml
|
|
|
|
- name: Check documentation coverage
|
|
id: docs
|
|
continue-on-error: true
|
|
run: |
|
|
# Generate documentation coverage report
|
|
interrogate -v --ignore-init-method --ignore-init-module --ignore-magic --ignore-private --ignore-property-decorators --ignore-semiprivate --fail-under=80 pyWebLayout/
|
|
|
|
- name: Lint with flake8
|
|
run: |
|
|
# Stop the build if there are Python syntax errors or undefined names
|
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
# Exit-zero treats all errors as warnings
|
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
|
|
|
- name: Create coverage info directory
|
|
if: always()
|
|
run: |
|
|
mkdir -p cov_info
|
|
echo "Created cov_info directory for coverage data"
|
|
|
|
- name: Update test coverage badge on success
|
|
if: steps.pytest.outcome == 'success' && always()
|
|
run: |
|
|
echo "Tests passed! Generating successful coverage badge..."
|
|
|
|
if [ -f coverage.json ]; then
|
|
coverage-badge -o cov_info/coverage.svg -f
|
|
echo "✅ Test coverage badge updated with actual results"
|
|
else
|
|
echo "⚠️ No coverage.json found, keeping failed badge"
|
|
fi
|
|
|
|
- name: Update docs coverage badge on success
|
|
if: steps.docs.outcome == 'success' && always()
|
|
run: |
|
|
echo "Docs check passed! Generating successful docs badge..."
|
|
|
|
# Remove existing badge first to avoid overwrite error
|
|
rm -f cov_info/coverage-docs.svg
|
|
interrogate --generate-badge cov_info/coverage-docs.svg pyWebLayout/
|
|
echo "✅ Docs coverage badge updated with actual results"
|
|
|
|
- name: Generate coverage reports
|
|
if: steps.pytest.outcome == 'success'
|
|
run: |
|
|
# Generate coverage summary for README
|
|
python -c "
|
|
import json
|
|
import os
|
|
# Read coverage data
|
|
if os.path.exists('coverage.json'):
|
|
with open('coverage.json', 'r') as f:
|
|
coverage_data = json.load(f)
|
|
total_coverage = round(coverage_data['totals']['percent_covered'], 1)
|
|
# Create coverage summary file in cov_info directory
|
|
with open('cov_info/coverage-summary.txt', 'w') as f:
|
|
f.write(f'{total_coverage}%')
|
|
print(f'Test Coverage: {total_coverage}%')
|
|
covered_lines = coverage_data['totals']['covered_lines']
|
|
total_lines = coverage_data['totals']['num_statements']
|
|
print(f'Lines Covered: {covered_lines}/{total_lines}')
|
|
else:
|
|
print('No coverage data found')
|
|
"
|
|
|
|
# Copy other coverage files to cov_info
|
|
if [ -f coverage.json ]; then cp coverage.json cov_info/; fi
|
|
if [ -f coverage.xml ]; then cp coverage.xml cov_info/; fi
|
|
if [ -d htmlcov ]; then cp -r htmlcov cov_info/; fi
|
|
|
|
- name: Final badge status
|
|
if: always()
|
|
run: |
|
|
echo "=== FINAL BADGE STATUS ==="
|
|
echo "Test outcome: ${{ steps.pytest.outcome }}"
|
|
echo "Docs outcome: ${{ steps.docs.outcome }}"
|
|
|
|
if [ -f cov_info/coverage.svg ]; then
|
|
echo "✅ Test coverage badge: $(ls -lh cov_info/coverage.svg)"
|
|
else
|
|
echo "❌ Test coverage badge: MISSING"
|
|
fi
|
|
|
|
if [ -f cov_info/coverage-docs.svg ]; then
|
|
echo "✅ Docs coverage badge: $(ls -lh cov_info/coverage-docs.svg)"
|
|
else
|
|
echo "❌ Docs coverage badge: MISSING"
|
|
fi
|
|
|
|
echo "Coverage info directory contents:"
|
|
ls -la cov_info/ 2>/dev/null || echo "No cov_info directory found"
|
|
|
|
- name: Upload coverage artifacts
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: coverage-reports
|
|
path: |
|
|
cov_info/
|
|
|
|
- name: Commit badges to badges branch
|
|
if: github.ref == 'refs/heads/master'
|
|
run: |
|
|
git config --local user.email "action@gitea.local"
|
|
git config --local user.name "Gitea Action"
|
|
|
|
# Set the remote URL to use the token
|
|
git remote set-url origin https://${{ secrets.PUSH_TOKEN }}@gitea.tourolle.paris/dtourolle/pyWebLayout.git
|
|
|
|
# Create a new orphan branch for badges (this discards any existing badges branch)
|
|
git checkout --orphan badges
|
|
|
|
# Remove all files except cov_info
|
|
find . -maxdepth 1 -not -name '.git' -not -name 'cov_info' -exec rm -rf {} + 2>/dev/null || true
|
|
|
|
# Add only the coverage info directory
|
|
git add -f cov_info/
|
|
|
|
# Always commit (force overwrite)
|
|
echo "Force updating badges branch with new coverage data..."
|
|
git commit -m "Update coverage badges [skip ci]"
|
|
git push -f origin badges
|