diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dff1cb17a2c..4995153001e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -159,3 +159,9 @@ updates: semver-major-days: 30 semver-minor-days: 7 semver-patch-days: 3 + - package-ecosystem: pre-commit + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eef87950f25..fa019069108 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -66,6 +66,9 @@ jobs: - name: check compilation without threading run: cargo check ${{ env.CARGO_ARGS }} + - run: cargo doc --locked + if: runner.os == 'Linux' + - name: check compilation without host_env (sandbox mode) run: | cargo check -p rustpython-vm --no-default-features --features compiler @@ -326,8 +329,13 @@ jobs: run: python -I scripts/whats_left.py ${{ env.CARGO_ARGS }} --features jit lint: - name: Lint Rust & Python code + name: Lint runs-on: ubuntu-latest + permissions: + contents: read + checks: write + pull-requests: write + security-events: write # for zizmor steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -337,53 +345,52 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} - - name: Check for redundant test patches - run: python scripts/check_redundant_patches.py - - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 with: - components: clippy toolchain: stable + components: rustfmt - - name: run clippy on wasm - run: cargo clippy --manifest-path=crates/wasm/Cargo.toml -- -Dwarnings - - - name: Ensure docs generate no warnings - run: cargo doc --locked + - uses: cargo-bins/cargo-binstall@113a77a4ce971c41332f2129c3d995df993cf746 # v1.17.8 - - name: Ensure Lib/_opcode_metadata is updated + - name: cargo shear run: | - python scripts/generate_opcode_metadata.py - if [ -n "$(git status --porcelain)" ]; then - exit 1 - fi + cargo binstall --no-confirm cargo-shear + cargo shear - - name: Install ruff - uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1 - with: - version: "0.15.5" - args: "--version" + - name: actionlint + uses: reviewdog/action-actionlint@0d952c597ef8459f634d7145b0b044a9699e5e43 # v1.71.0 - - run: ruff check --diff + - name: zizmor + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 - - run: ruff format --check + - name: restore prek cache + if: ${{ github.ref != 'refs/heads/main' }} # never restore on main + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + key: prek-${{ hashFiles('.pre-commit-config.yaml') }} + path: ~/.cache/prek - - name: install prettier - run: | - yarn global add prettier - yarn global bin >> "$GITHUB_PATH" + - name: prek + id: prek + uses: j178/prek-action@79f765515bd648eb4d6bb1b17277b7cb22cb6468 # v2.0.0 + with: + cache: false + show-verbose-logs: false + continue-on-error: true - - name: check wasm code with prettier - # prettier doesn't handle ignore files very well: https://github.com/prettier/prettier/issues/8506 - run: cd wasm && git ls-files -z | xargs -0 prettier --check -u - # Keep cspell check as the last step. This is optional test. - - name: install extra dictionaries - run: npm install @cspell/dict-en_us @cspell/dict-cpp @cspell/dict-python @cspell/dict-rust @cspell/dict-win32 @cspell/dict-shell - - name: spell checker - uses: streetsidesoftware/cspell-action@v8 + - name: save prek cache + if: ${{ github.ref == 'refs/heads/main' }} # only save on main + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - files: "**/*.rs" - incremental_files_only: true + key: prek-${{ hashFiles('.pre-commit-config.yaml') }} + path: ~/.cache/prek + + - name: reviewdog + uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # 1.24.0 + with: + level: warning + fail_level: error + cleanup: false miri: if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} @@ -425,12 +432,16 @@ jobs: - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 with: + components: clippy toolchain: stable - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} + - name: cargo clippy + run: cargo clippy --manifest-path=crates/wasm/Cargo.toml -- -Dwarnings + - name: install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: install geckodriver @@ -520,29 +531,3 @@ jobs: run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py" - name: run cpython unittest run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/release/rustpython.wasm -- "$(pwd)/Lib/test/test_int.py" - - cargo-shear: - name: cargo shear - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: cargo-bins/cargo-binstall@113a77a4ce971c41332f2129c3d995df993cf746 # v1.17.8 - - - run: cargo binstall --no-confirm cargo-shear - - - run: cargo shear - - security-lint: - runs-on: ubuntu-latest - permissions: - security-events: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Run zizmor - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 diff --git a/.github/workflows/pr-format.yaml b/.github/workflows/pr-format.yaml deleted file mode 100644 index 446b8a785c8..00000000000 --- a/.github/workflows/pr-format.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: Format Check - -# This workflow triggers when a PR is opened/updated -# Posts inline suggestion comments instead of auto-committing -on: - pull_request: - types: [opened, synchronize, reopened] - branches: - - main - - release - -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -env: - PYTHON_VERSION: "3.14.3" - -jobs: - format_check: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - uses: reviewdog/action-actionlint@0d952c597ef8459f634d7145b0b044a9699e5e43 # v1.71.0 - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - - name: Run cargo fmt - run: cargo fmt --all - - - name: Install ruff - uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1 - with: - version: "0.15.4" - args: "--version" - - - name: Run ruff format - run: ruff format - - - name: Run ruff check import sorting - run: ruff check --select I --fix - - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Run generate_opcode_metadata.py - run: python scripts/generate_opcode_metadata.py - - - name: Check for formatting changes - run: | - if ! git diff --exit-code; then - echo "::error::Formatting changes detected. Please run 'cargo fmt --all', 'ruff format', and 'ruff check --select I --fix' locally." - exit 1 - fi - - - name: Post formatting suggestions - if: failure() - uses: reviewdog/action-suggester@v1 - with: - tool_name: auto-format - github_token: ${{ secrets.GITHUB_TOKEN }} - level: warning - filter_mode: diff_context diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..00a0a194b87 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,71 @@ +# NOTE: Reason for not using `prek.toml` is dependabot supports `pre-commit` as an ecosystem +# See: https://github.blog/changelog/2026-03-10-dependabot-now-supports-pre-commit-hooks/ + +fail_fast: false +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + priority: 0 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.7 + hooks: + - id: ruff-format + priority: 0 + + - id: ruff-check + args: [--select, I, --fix, --exit-non-zero-on-fix] + types_or: [python] + require_serial: true + priority: 1 + + - repo: local + hooks: + - id: redundant-test-patches + name: check redundant test patches + entry: scripts/check_redundant_patches.py + files: '^Lib/test/.*\.py$' + language: script + types: [python] + priority: 0 + + - repo: local + hooks: + - id: rustfmt + name: rustfmt + entry: rustfmt + language: system + types: [rust] + priority: 0 + + - id: generate-opcode-metadata + name: generate opcode metadata + entry: python scripts/generate_opcode_metadata.py + files: '^(crates/compiler-core/src/bytecode/instruction\.rs|scripts/generate_opcode_metadata\.py)$' + pass_filenames: false + language: system + require_serial: true + priority: 1 # so rustfmt runs first + + - repo: https://github.com/streetsidesoftware/cspell-cli + rev: v9.7.0 + hooks: + - id: cspell + types: [rust] + additional_dependencies: + - '@cspell/dict-en_us' + - '@cspell/dict-cpp' + - '@cspell/dict-python' + - '@cspell/dict-rust' + - '@cspell/dict-win32' + - '@cspell/dict-shell' + priority: 0 + + - repo: https://github.com/rbubley/mirrors-prettier + rev: v3.8.1 + hooks: + - id: prettier + files: '^wasm/.*$' + priority: 0 diff --git a/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs b/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs index a152ebe1c4a..382daebb04e 100644 --- a/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs +++ b/example_projects/wasm32_without_js/rustpython-without-js/src/lib.rs @@ -1,4 +1,4 @@ -use rustpython_vm::{Interpreter}; +use rustpython_vm::Interpreter; unsafe extern "C" { fn kv_get(kp: i32, kl: i32, vp: i32, vl: i32) -> i32; @@ -37,12 +37,7 @@ pub unsafe extern "C" fn eval(s: *const u8, l: usize) -> i32 { let msg = format!("eval result: {result}"); - unsafe { - print( - msg.as_str().as_ptr() as usize as i32, - msg.len() as i32, - ) - }; + unsafe { print(msg.as_str().as_ptr() as usize as i32, msg.len() as i32) }; 0 } diff --git a/scripts/check_redundant_patches.py b/scripts/check_redundant_patches.py index 25cd2e1229e..4bc89a573d4 100644 --- a/scripts/check_redundant_patches.py +++ b/scripts/check_redundant_patches.py @@ -1,15 +1,46 @@ #!/usr/bin/env python +import argparse import ast +import glob +import os import pathlib import sys ROOT = pathlib.Path(__file__).parents[1] TEST_DIR = ROOT / "Lib" / "test" +IS_GH_CI = "GITHUB_ACTIONS" in os.environ -def main(): + +def build_argparser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="check_redundant_patches") + parser.add_argument( + "patterns", + nargs="*", + default=[f"{TEST_DIR}/**/*.py"], + help="Glob patterns (e.g. foo/bar/**.py a/b/file.py)", + ) + + return parser + + +def iter_files(patterns: list[str]): + seen = set() + for pattern in set(patterns): + matches = glob.glob(pattern, recursive=True) + for path in matches: + if path in seen: + continue + seen.add(path) + yield path + + +def main(patterns: list[str]): exit_status = 0 - for file in TEST_DIR.rglob("**/*.py"): + for file in map(pathlib.Path, iter_files(patterns)): + if file.is_dir(): + continue + try: contents = file.read_text(encoding="utf-8") except UnicodeDecodeError: @@ -20,7 +51,11 @@ def main(): except SyntaxError: continue + cls_name = None for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + cls_name = node.name + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): continue @@ -39,14 +74,19 @@ def main(): f"super().{name}()", ): exit_status += 1 - rel = file.relative_to(ROOT) + lineno = node.lineno - print( - f"{rel}:{name}:{lineno} is a test patch that can be safely removed", - file=sys.stderr, - ) + msg = f"{file}:{lineno}:{cls_name}.{name} is a test patch that can be safely removed" + if IS_GH_CI: + end_lineno = node.end_lineno + msg = f"::error file={file},line={lineno},endLine={end_lineno},title=Redundant Test Patch::{msg}" + + print(msg, file=sys.stderr) + return exit_status if __name__ == "__main__": - exit(main()) + parser = build_argparser() + args = parser.parse_args() + exit(main(args.patterns))