diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f8a1f61 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,120 @@ +name: Publish to PyPI + +on: + release: + types: + - published + workflow_dispatch: + inputs: + tag: + description: "Tag version (e.g., v0.1.0)" + required: false + type: string + +env: + CARGO_TERM_COLOR: always + +jobs: + verify-version: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Verify tag matches package version + run: | + if [ -n "${{ inputs.tag }}" ]; then + TAG_VERSION="${{ inputs.tag }}" + TAG_VERSION="${TAG_VERSION#v}" + else + TAG_VERSION="${GITHUB_REF_NAME#v}" + fi + PACKAGE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") + echo "Tag version: ${TAG_VERSION}" + echo "Package version: ${PACKAGE_VERSION}" + if [ "${TAG_VERSION}" != "${PACKAGE_VERSION}" ]; then + echo "::error::Tag version (${TAG_VERSION}) must match package version (${PACKAGE_VERSION})" + exit 1 + fi + + build-wheels: + needs: verify-version + name: Build wheels on ${{ matrix.os }} (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + # Linux + - os: ubuntu-latest + target: x86_64 + - os: ubuntu-latest + target: aarch64 + # macOS + - os: macos-13 # Intel + target: x86_64 + - os: macos-14 # Apple Silicon + target: aarch64 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Build wheels + uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: "true" + manylinux: auto + + - name: Upload wheels + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-${{ matrix.os }}-${{ matrix.target }} + path: dist + + build-sdist: + needs: verify-version + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Build sdist + uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + with: + command: sdist + args: --out dist + + - name: Upload sdist + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sdist + path: dist + + publish: + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + permissions: + id-token: write # Required for trusted publishing + steps: + - name: Download artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: dist + merge-multiple: true + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b262fdb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,113 @@ +name: Release + +on: + push: + tags: + - "v*" + +env: + CARGO_TERM_COLOR: always + +jobs: + verify-version: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Verify tag matches package version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PACKAGE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") + echo "Tag version: ${TAG_VERSION}" + echo "Package version: ${PACKAGE_VERSION}" + if [ "${TAG_VERSION}" != "${PACKAGE_VERSION}" ]; then + echo "::error::Tag version (${TAG_VERSION}) must match package version (${PACKAGE_VERSION})" + exit 1 + fi + + build-wheels: + needs: verify-version + name: Build wheels on ${{ matrix.os }} (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + # Linux + - os: ubuntu-latest + target: x86_64 + - os: ubuntu-latest + target: aarch64 + # macOS + - os: macos-13 # Intel + target: x86_64 + - os: macos-14 # Apple Silicon + target: aarch64 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Build wheels + uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: "true" + manylinux: auto + + - name: Upload wheels + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wheels-${{ matrix.os }}-${{ matrix.target }} + path: dist + + build-sdist: + needs: verify-version + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Build sdist + uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1 + with: + command: sdist + args: --out dist + + - name: Upload sdist + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sdist + path: dist + + release: + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: artifacts + merge-multiple: true + + - name: Publish GitHub Release + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + with: + files: artifacts/* + generate_release_notes: true + draft: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb99f5e..61a464e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,8 @@ Thank you for your interest in contributing! This document provides guidelines a ```bash # Clone the repository -git clone https://github.com/USERNAME/rope.git -cd rope +git clone https://github.com/K-dash/pyropust.git +cd pyropust # Install Python dependencies uv sync @@ -27,17 +27,19 @@ makers dev ## Development Commands -All development tasks are managed via `cargo-make`. Run `makers ` to execute a task. +All development tasks are managed via `cargo-make`. In practice, contributors should use `makers` for nearly everything. -### Build Tasks +### Recommended workflow ```bash -makers dev # Development build (maturin develop) -makers build # Same as dev -makers build-release # Build release wheel -makers clean # Clean all build artifacts +makers # Full pipeline (gen + build + format + lint + test) +makers dev # Faster dev build (maturin develop) +makers gen # Regenerate operator bindings after editing kind.rs +makers clean # Clean build artifacts ``` +If you need a specific task beyond the above, check `Makefile.toml` for the full list. + ### Code Generation **Important**: This project uses automatic code generation for operators. @@ -66,7 +68,7 @@ MyOp { arg: String }, 2. Add implementation to `src/ops/apply.rs` 3. Run `makers gen` to regenerate code -4. Run `makers all` to verify everything works +4. Run `makers` to verify everything works **When adding a new namespace (e.g., `@ns json`):** @@ -77,43 +79,7 @@ MyOp { arg: String }, - `src/lib.rs` (adds `m.add_class::()`) - `pyropust/__init__.pyi` (adds type stubs) 3. **Manual step**: Add `OpJson` to the `use py::{...}` import in `src/lib.rs` -4. Run `makers all` to verify everything works - -### Formatting - -```bash -makers rust-fmt # Format Rust code -makers rust-fmt-check # Check Rust formatting -makers ruff-fmt # Format Python code -makers ruff-fmt-check # Check Python formatting -makers fmt-all # Format all code (Rust + Python) -``` - -### Linting - -```bash -makers rust-clippy # Run Rust linter (clippy) -makers ruff # Run Python linter (ruff) -makers lint-all # Run all linters -``` - -### Testing - -```bash -makers rust-test # Run Rust unit tests -makers pytest # Run Python tests -makers mypy # Run MyPy type checker (strict) -makers pyright # Run Pyright type checker (strict) -makers test-all # Run all tests (Rust + Python) -``` - -### CI/Complete Checks - -```bash -makers check # Run all checks (format, lint, test, gen) -makers all # Run complete pipeline (gen + dev + check) -makers ci # Same as all -``` +4. Run `makers` to verify everything works ## Code Generation System @@ -168,46 +134,6 @@ The CI automatically checks if generated code is up-to-date: - `check-generated` job runs `makers gen` and verifies no diffs - PRs that modify `kind.rs` without running `makers gen` will fail -## Project Structure - -``` -pyropust/ -├── .github/ -│ └── workflows/ -│ └── ci.yml # GitHub Actions CI -├── pyropust/ # Python package -│ ├── __init__.py # Public API -│ ├── __init__.pyi # PEP 561 type stubs -│ ├── do.py # @do decorator -│ ├── pyropust_native.pyi # Internal native module stubs -│ └── py.typed # PEP 561 marker -├── src/ # Rust implementation -│ ├── lib.rs # PyO3 module definition -│ ├── data/ # Value type and Python conversion -│ ├── ops/ # Operator implementations -│ │ ├── kind.rs # Operator metadata (SSOT) -│ │ ├── apply.rs # Operator execution logic -│ │ └── error.rs # Error handling -│ └── py/ # PyO3 exposed classes -│ ├── blueprint.rs # Blueprint class -│ ├── error.rs # RopustError class -│ ├── op_generated.rs # Generated Op class -│ ├── operator.rs # Operator class -│ └── result.rs # Result/Option classes -├── tests/ -│ ├── test_blueprint.py # Blueprint runtime tests -│ ├── test_runtime.py # Result/Option tests -│ └── typing/ # Type checker tests -│ ├── test_typing_mypy.py -│ └── test_typing_pyright.py -├── tools/ -│ └── gen_ops.py # Code generator -├── Cargo.toml # Rust dependencies -├── pyproject.toml # Python project config -├── Makefile.toml # cargo-make task definitions -└── rust-toolchain.toml # Rust version pin -``` - ## Type Safety Principles 1. **`Op.assert_*` methods are validators, not converters**: They return `Err(RopustError)` if preconditions aren't met @@ -215,21 +141,6 @@ pyropust/ 3. **`.pipe()` only connects compatible types**: Type checkers verify the pipeline at build time 4. **For dynamic input, use guards**: `Blueprint.any().guard_str()` explicitly narrows types -## Testing Guidelines - -### Running Tests - -```bash -# All tests -makers test-all - -# Specific test suites -makers pytest # Python runtime tests -makers mypy # Type checking with MyPy -makers pyright # Type checking with Pyright -makers rust-test # Rust unit tests -``` - ### Type Checker Tests Located in `tests/typing/`, these tests verify that type checkers correctly infer types: @@ -244,13 +155,7 @@ assert_type(bp, Blueprint[str, object]) ## CI Pipeline -The CI runs three parallel jobs: - -1. **check-generated**: Verifies generated code is up-to-date -2. **rust**: Runs `cargo fmt`, `clippy`, and `test` -3. **python**: Runs Ruff, MyPy, Pyright, and Pytest - -All jobs must pass for a PR to be merged. +CI runs the same `makers` pipeline used locally, so if it passes on your machine, it should pass in CI. ## Common Issues diff --git a/pyproject.toml b/pyproject.toml index b13eea6..377b632 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ readme = "README.md" requires-python = ">=3.12" authors = [{ name = "K-dash" }] license = { text = "MIT" } +keywords = ["rust", "result", "option", "error-handling", "type-safety", "pyo3"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", @@ -22,6 +23,11 @@ classifiers = [ "Typing :: Typed", ] +[project.urls] +Homepage = "https://github.com/K-dash/pyropust" +Repository = "https://github.com/K-dash/pyropust" +Issues = "https://github.com/K-dash/pyropust/issues" + [project.optional-dependencies] dev = [ "maturin>=1.6",