diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab79007 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + pull_request: + branches: + - master + - main + push: + branches: + - master + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.10', '3.11', '3.12'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install dependencies + run: | + uv sync --dev + + # - name: Run code formatting check + # run: | + # uv run ruff format --check . + + # - name: Run linting + # run: | + # uv run ruff check . + + - name: Run type checking + run: | + uv run mypy + + - name: Run tests + run: | + make test + + - name: Run integration tests + run: | + uv run python -m pytest tests/integration/ + + - name: Run typeguard + if: matrix.os == 'ubuntu-latest' + run: | + make typeguard + + publish-check: + runs-on: ubuntu-latest + needs: test + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Check if version exists on PyPI + id: version_check + run: | + VERSION=$(uv version | awk '{print $2}') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Check if version already exists on PyPI + if curl -s "https://pypi.org/pypi/pytest-api-cov/$VERSION/json" > /dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Version $VERSION already published to PyPI" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Version $VERSION not found on PyPI" + fi + + - name: Comment on publish workflow + if: steps.version_check.outputs.exists == 'true' + run: | + echo "::warning::Version ${{ steps.version_check.outputs.version }} already exists on PyPI. Update version in pyproject.toml before publishing." + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..705b10c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,111 @@ +name: Publish to PyPI + +on: + push: + branches: + - master + - main + workflow_dispatch: # Allow manual triggering + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: write # Required to create and push tags + id-token: write # Required for OIDC authentication (if using PyPI trusted publishing) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for tags + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Read version from pyproject.toml + id: version + run: | + VERSION=$(uv version | awk '{print $2}') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Package version: $VERSION" + + - name: Check if version already published + id: check_published + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Check if Git tag exists + if git rev-parse "v$VERSION" >/dev/null 2>&1; then + echo "tag_exists=true" >> $GITHUB_OUTPUT + echo "Git tag v$VERSION already exists" + else + echo "tag_exists=false" >> $GITHUB_OUTPUT + echo "Git tag v$VERSION does not exist" + fi + + # Check if version exists on PyPI + if curl -s "https://pypi.org/pypi/pytest-api-cov/$VERSION/json" > /dev/null 2>&1; then + echo "pypi_exists=true" >> $GITHUB_OUTPUT + echo "Version $VERSION already published to PyPI" + else + echo "pypi_exists=false" >> $GITHUB_OUTPUT + echo "Version $VERSION not found on PyPI" + fi + + - name: Create version tag + if: steps.check_published.outputs.tag_exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v${{ steps.version.outputs.version }}" -m "Release version ${{ steps.version.outputs.version }}" + git push origin "v${{ steps.version.outputs.version }}" + + - name: Set up build environment + if: steps.check_published.outputs.pypi_exists == 'false' + run: | + uv sync --dev + + - name: Run pipeline (tests, linting, etc.) + if: steps.check_published.outputs.pypi_exists == 'false' + run: make pipeline + + - name: Build package + if: steps.check_published.outputs.pypi_exists == 'false' + run: make build + + - name: Publish to PyPI + if: steps.check_published.outputs.pypi_exists == 'false' + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: | + if [ -z "$PYPI_TOKEN" ]; then + echo "❌ PYPI_TOKEN secret is not set. Please configure it in repository settings." + echo " Settings → Secrets and variables → Actions → New repository secret" + exit 1 + fi + echo $PYPI_TOKEN > /tmp/pypi_token.txt + uv publish --token $(cat /tmp/pypi_token.txt) + rm /tmp/pypi_token.txt + + - name: Verify publication + if: steps.check_published.outputs.pypi_exists == 'false' + run: | + sleep 10 # Wait for PyPI to index the package + uv run --with pytest-api-cov --no-project -- python -c \ + "import pytest_api_cov; print(f'✅ Published version: {pytest_api_cov.__version__}')" + + - name: Skip publish - already published + if: steps.check_published.outputs.pypi_exists == 'true' + run: | + echo "✅ Version ${{ steps.version.outputs.version }} already published to PyPI. Skipping publish." + diff --git a/Makefile b/Makefile index ccc7cf4..e8d3bc8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ # Makefile -.PHONY: ruff mypy test clean clean-all +.PHONY: ruff mypy test clean clean-all version PYPI_TOKEN := $(shell type .pypi_token 2>nul || echo "") TEST_PYPI_TOKEN := $(shell type .test_pypi_token 2>nul || echo "") +version: + @uv version + ruff: @echo "Running ruff..." @uv run ruff format . diff --git a/PUBLISH.md b/PUBLISH.md new file mode 100644 index 0000000..16230d4 --- /dev/null +++ b/PUBLISH.md @@ -0,0 +1,177 @@ +# Publishing Guide + +This document explains how the automatic publishing workflow works and how to set it up. + +## Overview + +The repository uses GitHub Actions to automatically publish new versions to PyPI when code is pushed to the `master` or `main` branch. The workflow reads the version from `pyproject.toml` and creates a corresponding Git tag. + +## How It Works + +1. **Version Detection**: The workflow reads the version from `pyproject.toml` using `uv version` command (available in uv 0.8+). + +2. **Check Existing Versions**: + - Checks if a Git tag `v{version}` already exists + - Checks if the version is already published to PyPI + +3. **Create Git Tag**: If the tag doesn't exist, creates and pushes a `v{version}` tag to the repository + +4. **Run Tests**: Executes the full test pipeline (formatting, linting, type checking, tests) + +5. **Build Package**: Builds the package using `uv build` + +6. **Publish to PyPI**: Publishes to PyPI using `uv publish` + +7. **Verify**: Verifies the package was published successfully + +## Setup Instructions + +### 1. Create PyPI API Token + +1. Go to [PyPI Account Settings](https://pypi.org/manage/account/) +2. Scroll down to "API tokens" +3. Click "Add API token" +4. Create a token with a name like "GitHub Actions publish" +5. Select "Entire account" scope +6. Copy the token (you'll only see it once!) + +### 2. Add GitHub Secret + +**Use a Repository Secret** (not Environment Secret) for this use case. + +1. Go to your GitHub repository +2. Navigate to: **Settings** → **Secrets and variables** → **Actions** +3. Click **"New repository secret"** +4. Name: `PYPI_TOKEN` +5. Value: Paste your PyPI API token +6. Click **Add secret** + +### 3. Enable GitHub Actions + +The workflows are automatically enabled when you push to GitHub. You can verify they exist: + +- `.github/workflows/publish.yml` - Publishes to PyPI +- `.github/workflows/ci.yml` - Runs tests on PRs and pushes + +## Publishing Process + +### Automatic Publishing + +Pushing to `master` or `main` triggers automatic publishing if: + +- The version in `pyproject.toml` doesn't exist on PyPI yet +- The version tag doesn't exist in Git +- All tests pass +- The `PYPI_TOKEN` secret is configured + +### Manual Publishing + +You can manually trigger a publish by: + +1. Going to **Actions** tab in GitHub +2. Selecting "Publish to PyPI" workflow +3. Clicking **Run workflow** button +4. Selecting the branch (master/main) +5. Clicking **Run workflow** + +### Publishing a New Version + +To release a new version: + +1. **Update version in `pyproject.toml`**: + ```toml + [project] + name = "pytest-api-cov" + version = "1.2.0" # ← Change this + ``` + +2. **Commit and push to master**: + ```bash + git add pyproject.toml + git commit -m "Bump version to 1.2.0" + git push origin master + ``` + +3. **Wait for GitHub Actions** to run automatically + +## Workflow Files + +### `.github/workflows/publish.yml` + +Main publishing workflow triggered on pushes to master/main. + +**Key features:** +- Reads version from `pyproject.toml` +- Checks if already published (skips if duplicate version) +- Creates Git tag automatically +- Runs full test pipeline +- Builds and publishes to PyPI +- Verifies publication + +### `.github/workflows/ci.yml` + +Continuous integration for pull requests. + +**Key features:** +- Runs on multiple Python versions (3.10, 3.11, 3.12) +- Runs on multiple OS (Ubuntu, Windows, macOS) +- Checks code formatting +- Runs linting +- Runs type checking +- Runs unit and integration tests +- Warns if version already exists on PyPI + +### Version Detection + +The workflows use `uv version` (available in uv 0.8+) to extract the version directly from `pyproject.toml`. No additional scripts needed! + +## Troubleshooting + +### "PYPI_TOKEN secret is not set" + +**Solution**: Add the `PYPI_TOKEN` secret in GitHub repository settings as described above. + +### "Version already exists on PyPI" + +**Solution**: Bump the version in `pyproject.toml` and try again. + +### "Tests failing" + +**Solution**: Fix the failing tests. The workflow won't publish if tests fail. + +### "Git tag already exists" + +**Solution**: The workflow will skip creating the tag if it already exists. This is safe to ignore. + +## Manual Publishing (Without GitHub Actions) + +If you need to publish manually: + +```bash +# Run the full pipeline +make pipeline + +# Build the package +make build + +# Set your PyPI token +export PYPI_TOKEN="your-token-here" +echo $PYPI_TOKEN > .pypi_token + +# Publish +make publish +``` + +## Publishing to Test PyPI + +To publish to Test PyPI instead: + +```bash +# Set your Test PyPI token +echo $TEST_PYPI_TOKEN > .test_pypi_token + +# Publish to Test PyPI +make publish-test +``` + +Or create a separate workflow by copying `.github/workflows/publish.yml` and modifying it to use `TEST_PYPI_TOKEN` and the `--index testpypi` flag. diff --git a/pyproject.toml b/pyproject.toml index 9115a38..abf93f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytest-api-cov" -version = "1.1.3" +version = "1.1.4" description = "Pytest Plugin to provide API Coverage statistics for Python Web Frameworks" readme = "README.md" authors = [{ name = "Barnaby Gill", email = "barnabasgill@gmail.com" }] diff --git a/src/pytest_api_cov/frameworks.py b/src/pytest_api_cov/frameworks.py index a12bc4f..dad816a 100644 --- a/src/pytest_api_cov/frameworks.py +++ b/src/pytest_api_cov/frameworks.py @@ -93,11 +93,9 @@ def get_framework_adapter(app: Any) -> BaseAdapter: app_type = type(app).__name__ module_name = getattr(type(app), "__module__", "").split(".")[0] - if module_name == "flask" and app_type == "Flask": + if (module_name == "flask" and app_type == "Flask") or (module_name == "flask_openapi3" and app_type == "OpenAPI"): return FlaskAdapter(app) - elif module_name == "flask_openapi3" and app_type == "OpenAPI": - return FlaskAdapter(app) - elif module_name == "fastapi" and app_type == "FastAPI": + if module_name == "fastapi" and app_type == "FastAPI": return FastAPIAdapter(app) raise TypeError(f"Unsupported application type: {app_type}. pytest-api-coverage supports Flask and FastAPI.") diff --git a/src/pytest_api_cov/plugin.py b/src/pytest_api_cov/plugin.py index e19ee73..1e81651 100644 --- a/src/pytest_api_cov/plugin.py +++ b/src/pytest_api_cov/plugin.py @@ -24,9 +24,11 @@ def is_supported_framework(app: Any) -> bool: app_type = type(app).__name__ module_name = getattr(type(app), "__module__", "").split(".")[0] - return ((module_name == "flask" and app_type == "Flask") or - (module_name == "flask_openapi3" and app_type == "OpenAPI") or - (module_name == "fastapi" and app_type == "FastAPI")) + return ( + (module_name == "flask" and app_type == "Flask") + or (module_name == "flask_openapi3" and app_type == "OpenAPI") + or (module_name == "fastapi" and app_type == "FastAPI") + ) def auto_discover_app() -> Optional[Any]: diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 3e06b12..82a2b32 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -3,9 +3,9 @@ import os from unittest.mock import Mock, patch -from path import Path import pytest import tomli +from path import Path from pydantic import ValidationError from pytest_api_cov.config import ( diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 5b51f86..fed3aa4 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -21,14 +21,6 @@ class TestSupportedFramework: """Tests for framework detection utility functions.""" - def test_package_version(self): - """Test that package version is accessible.""" - import pytest_api_cov - - assert hasattr(pytest_api_cov, "__version__") - assert isinstance(pytest_api_cov.__version__, str) - assert pytest_api_cov.__version__ == "1.1.3" - def test_is_supported_framework_none(self): """Test framework detection with None.""" assert is_supported_framework(None) is False diff --git a/uv.lock b/uv.lock index 710bcad..e07be27 100644 --- a/uv.lock +++ b/uv.lock @@ -556,7 +556,7 @@ wheels = [ [[package]] name = "pytest-api-cov" -version = "1.1.3" +version = "1.1.4" source = { editable = "." } dependencies = [ { name = "fastapi" },