From 399262ec759f5d160af6bca5979926714d20ef45 Mon Sep 17 00:00:00 2001 From: Andrej Orsula Date: Sat, 20 Jan 2024 13:46:58 +0100 Subject: [PATCH] Initial commit Signed-off-by: Andrej Orsula --- .git_hooks/install.bash | 15 + .github/dependabot.yml | 11 + .github/workflows/pre-commit.yml | 32 + .github/workflows/rust.yml | 143 +++ .gitignore | 29 + .pre-commit-config.yaml | 76 ++ Cargo.lock | 1043 ++++++++++++++++++ Cargo.toml | 41 + LICENSE-APACHE | 176 +++ LICENSE-MIT | 21 + README.md | 175 +++ deny.toml | 53 + pyo3_bindgen/Cargo.toml | 25 + pyo3_bindgen/src/lib.rs | 82 ++ pyo3_bindgen_cli/Cargo.toml | 27 + pyo3_bindgen_cli/src/main.rs | 86 ++ pyo3_bindgen_cli/tests/cli.rs | 64 ++ pyo3_bindgen_engine/Cargo.toml | 36 + pyo3_bindgen_engine/benches/bindgen.rs | 107 ++ pyo3_bindgen_engine/build.rs | 4 + pyo3_bindgen_engine/src/bindgen.rs | 127 +++ pyo3_bindgen_engine/src/bindgen/attribute.rs | 131 +++ pyo3_bindgen_engine/src/bindgen/class.rs | 197 ++++ pyo3_bindgen_engine/src/bindgen/function.rs | 253 +++++ pyo3_bindgen_engine/src/bindgen/module.rs | 274 +++++ pyo3_bindgen_engine/src/build_utils.rs | 56 + pyo3_bindgen_engine/src/lib.rs | 8 + pyo3_bindgen_engine/src/types.rs | 406 +++++++ pyo3_bindgen_engine/tests/bindgen.rs | 261 +++++ pyo3_bindgen_macros/Cargo.toml | 24 + pyo3_bindgen_macros/src/lib.rs | 35 + pyo3_bindgen_macros/src/parser.rs | 20 + 32 files changed, 4038 insertions(+) create mode 100755 .git_hooks/install.bash create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 deny.toml create mode 100644 pyo3_bindgen/Cargo.toml create mode 100644 pyo3_bindgen/src/lib.rs create mode 100644 pyo3_bindgen_cli/Cargo.toml create mode 100644 pyo3_bindgen_cli/src/main.rs create mode 100644 pyo3_bindgen_cli/tests/cli.rs create mode 100644 pyo3_bindgen_engine/Cargo.toml create mode 100644 pyo3_bindgen_engine/benches/bindgen.rs create mode 100644 pyo3_bindgen_engine/build.rs create mode 100644 pyo3_bindgen_engine/src/bindgen.rs create mode 100644 pyo3_bindgen_engine/src/bindgen/attribute.rs create mode 100644 pyo3_bindgen_engine/src/bindgen/class.rs create mode 100644 pyo3_bindgen_engine/src/bindgen/function.rs create mode 100644 pyo3_bindgen_engine/src/bindgen/module.rs create mode 100644 pyo3_bindgen_engine/src/build_utils.rs create mode 100644 pyo3_bindgen_engine/src/lib.rs create mode 100644 pyo3_bindgen_engine/src/types.rs create mode 100644 pyo3_bindgen_engine/tests/bindgen.rs create mode 100644 pyo3_bindgen_macros/Cargo.toml create mode 100644 pyo3_bindgen_macros/src/lib.rs create mode 100644 pyo3_bindgen_macros/src/parser.rs diff --git a/.git_hooks/install.bash b/.git_hooks/install.bash new file mode 100755 index 0000000..a4b0745 --- /dev/null +++ b/.git_hooks/install.bash @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" &>/dev/null && pwd)" +REPOSITORY_DIR="$(dirname "${SCRIPT_DIR}")" + +# Install pre-commit if not detected +if ! command -v pre-commit >/dev/null 2>&1; then + pip install --user pre-commit +fi + +# Install local git hooks for this repository +cd "${REPOSITORY_DIR}" +pre-commit install --install-hooks --config "${REPOSITORY_DIR}/.pre-commit-config.yaml" +cd - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a1cd7d2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..cdab919 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,32 @@ +name: pre-commit + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.10" + SKIP: cargo-fmt,cargo-update,cargo-clippy,cargo-check,cargo-test,cargo-test-doc,cargo-doc,cargo-miri-test,cargo-miri-run,cargo-deny-check + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + ## Run pre-commit and try to apply fixes + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 + - name: Apply fixes from pre-commit + uses: pre-commit-ci/lite-action@v1.0.1 + if: always() diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..6d6a5a4 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,143 @@ +name: Rust + +on: + push: + branches: + - main + pull_request: + release: + types: [published] + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.10" + CARGO_TERM_COLOR: always + LIB_PACKAGE_NAME: pyo3_bindgen + CLI_PACKAGE_NAME: pyo3_bindgen_cli + ENGINE_PACKAGE_NAME: pyo3_bindgen_engine + MACROS_PROCEDURAL_PACKAGE_NAME: pyo3_bindgen_macros + +jobs: + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + ## cargo fmt + - name: cargo fmt + run: cargo fmt --all --check --verbose + + cargo: + needs: rustfmt + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + toolchain: + - "1.70" # Minimal supported Rust version (MSRV) + - stable + - beta + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name == 'push'}} + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + components: clippy + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + ## cargo check + - name: cargo check + run: cargo check --workspace --all-targets --verbose + - name: cargo check --no-default-features + run: cargo check --workspace --all-targets --no-default-features --verbose + - name: cargo check --all-features + run: cargo check --workspace --all-targets --all-features --verbose + + ## cargo test + - name: cargo test + run: cargo test --workspace --all-targets --verbose + - name: cargo test --no-default-features + run: cargo test --workspace --all-targets --no-default-features --verbose + - name: cargo test --all-features + run: cargo test --workspace --all-targets --all-features --verbose + + ## cargo test --doc + - name: cargo test --doc + run: cargo test --workspace --doc --verbose + - name: cargo test --doc --no-default-features + run: cargo test --workspace --doc --no-default-features --verbose + - name: cargo test --doc --all-features + run: cargo test --workspace --doc --all-features --verbose + + ## [stable] cargo clippy + - name: stable | cargo clippy + if: ${{ matrix.toolchain == 'stable' }} + run: cargo clippy --workspace --all-targets --all-features --no-deps --verbose -- --deny warnings + + ## [stable] cargo doc + - name: stable | cargo doc --document-private-items + if: ${{ matrix.toolchain == 'stable' }} + run: cargo doc --workspace --all-features --no-deps --document-private-items --verbose + + ## [stable] Code coverage + - name: stable | Install cargo llvm-cov for code coverage + uses: taiki-e/install-action@cargo-llvm-cov + if: ${{ matrix.toolchain == 'stable' }} + ## [stable] Generate coverage with cargo llvm-cov + - name: stable | Generate coverage + if: ${{ matrix.toolchain == 'stable' }} + run: cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info + ## [stable] Upload coverage to codecov.io + - name: stable | Upload coverage + if: ${{ matrix.toolchain == 'stable' }} + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: lcov.info + fail_ci_if_error: true + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check bans licenses sources + + publish: + if: ${{ github.event_name == 'release' }} + needs: + - cargo + - deny + runs-on: ubuntu-latest + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + ## Publish to crates.io + - name: Publish crate (engine) + if: ${{ env.CARGO_REGISTRY_TOKEN != '' }} + run: cargo publish --no-verify --package ${{ env.ENGINE_PACKAGE_NAME }} --token ${{ secrets.CARGO_REGISTRY_TOKEN }} + - name: Publish crate (procedural macros) + if: ${{ env.CARGO_REGISTRY_TOKEN != '' }} + run: cargo publish --no-verify --package ${{ env.MACROS_PROCEDURAL_PACKAGE_NAME }} --token ${{ secrets.CARGO_REGISTRY_TOKEN }} + - name: Publish crate (public API) + if: ${{ env.CARGO_REGISTRY_TOKEN != '' }} + run: cargo publish --no-verify --package ${{ env.LIB_PACKAGE_NAME }} --token ${{ secrets.CARGO_REGISTRY_TOKEN }} + - name: Publish crate (CLI tool) + if: ${{ env.CARGO_REGISTRY_TOKEN != '' }} + run: cargo publish --no-verify --package ${{ env.CLI_PACKAGE_NAME }} --token ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..340e0c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +## Rust & Cargo +target/ +**/*.rs.bk +**/*.pdb +**/.cargo/config.toml +!.cargo/config.toml + +## Python +**/__pycache__/ +**/.pytest_cache/ +**/*.pcd +**/*.py[cod] +**/*.pyc +**/*$py.class + +## Shared Objects +**/*.so + +## Virtual environments +.env/ +.venv/ + +## VS Code +.vscode/* +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/*.code-snippets diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0e04085 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,76 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +exclude: Cargo.lock +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + exclude: \.rs$ + - id: check-symlinks + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: name-tests-test + - id: requirements-txt-fixer + - id: sort-simple-yaml + - id: trailing-whitespace + + - repo: https://github.com/lovesegfault/beautysh + rev: v6.2.1 + hooks: + - id: beautysh + + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: ["--ignore-words-list", "crate"] + exclude: Cargo.lock + + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 + hooks: + - id: mdformat + + - repo: https://github.com/AndrejOrsula/pre-commit-cargo + rev: 0.3.0 + hooks: + - id: cargo-fmt + - id: cargo-update + - id: cargo-clippy + args: ["--workspace", "--all-targets", "--", "--deny=warnings"] + - id: cargo-check + args: ["--workspace", "--all-targets"] + - id: cargo-test + args: ["--workspace", "--all-targets"] + - id: cargo-test-doc + args: ["--workspace"] + - id: cargo-doc + args: ["--workspace", "--no-deps", "--document-private-items"] + - id: cargo-deny-check diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..75b9b12 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1043 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "assert_cmd" +version = "2.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00ad3f3a942eee60335ab4342358c161ee296829e0d16ff42fc1d6cb07815467" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +dependencies = [ + "cfg-if", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3_bindgen" +version = "0.1.0" +dependencies = [ + "pyo3_bindgen_engine", + "pyo3_bindgen_macros", +] + +[[package]] +name = "pyo3_bindgen_cli" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap", + "predicates", + "prettyplease", + "pyo3_bindgen", + "syn", +] + +[[package]] +name = "pyo3_bindgen_engine" +version = "0.1.0" +dependencies = [ + "criterion", + "indoc", + "itertools 0.12.0", + "prettyplease", + "proc-macro2", + "pyo3", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "pyo3_bindgen_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "pyo3", + "pyo3_bindgen_engine", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "web-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6368a3b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[workspace] +members = [ + # Public API + "pyo3_bindgen", + # CLI tool + "pyo3_bindgen_cli", + # The underlying engine + "pyo3_bindgen_engine", + # Procedural macros + "pyo3_bindgen_macros", +] +resolver = "2" + +[workspace.package] +authors = ["Andrej Orsula "] +categories = ["development-tools::ffi"] +edition = "2021" +keywords = ["bindgen", "ffi", "pyo3", "python"] +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/AndrejOrsula/pyo3_bindgen" +rust-version = "1.70" +version = "0.1.0" + +[workspace.dependencies] +pyo3_bindgen = { path = "pyo3_bindgen", version = "0.1.0" } +pyo3_bindgen_engine = { path = "pyo3_bindgen_engine", version = "0.1.0" } +pyo3_bindgen_macros = { path = "pyo3_bindgen_macros", version = "0.1.0" } + +assert_cmd = { version = "2" } +clap = { version = "4.4", features = ["derive"] } +criterion = { version = "0.5" } +indoc = { version = "2" } +itertools = { version = "0.12" } +predicates = { version = "3" } +prettyplease = { version = "0.2" } +proc-macro2 = { version = "1" } +pyo3 = { version = "0.20", default-features = false } +pyo3-build-config = { version = "0.20", features = ["resolve-config"] } +quote = { version = "1" } +syn = { version = "2" } diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..1133824 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Andrej Orsula + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..682646d --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +# pyo3_bindgen + +

+ crates.io + Rust + codecov +

+ +Automatic generation of Rust FFI bindings to Python modules via [PyO3](https://pyo3.rs). Python modules are analyzed recursively to generate Rust bindings with an identical structure for all public classes, functions, properties, and constants. Any available docstrings and type annotations are also preserved in their Rust equivalents. + +An example of a generated Rust function signature and its intended usage is shown below. Of course, manually wrapping parts of the generated bindings in a more idiomatic Rust API might be beneficial in most cases. + + + + + + + +
Source code (Python) Generated code (Rust)
+ +```py +  +def answer_to(question: str) -> int: + """Returns answer to question.""" + + return 42 + +  +``` + +______________________________________________________________________ + +```py +  +def main(): + assert answer_to("life") == 42 + + +if __name__ == "__main__": + main() +  +``` + + + +```rs +/// Returns answer to question. +pub fn answer_to<'py>( + py: ::pyo3::marker::Python<'py>, + question: &str, +) -> ::pyo3::PyResult { + ... // Calls function via `pyo3` +} +``` + +______________________________________________________________________ + +```rs +pub fn main() -> pyo3::PyResult<()> { + pyo3::Python::with_gil(|py| { + assert_eq!( + answer_to(py, "universe")?, 42 + ); + Ok(()) + }) +} +``` + +
+ +This project is intended to simplify the integration or transition of existing Python codebases into Rust. You, as a developer, gain immediate access to the Rust type system and countless other benefits of modern compiled languages with the generated bindings. Furthermore, the entire stock of high-quality crates from [crates.io](https://crates.io) becomes at your disposal. + +On its own, the generated Rust code does not provide any performance benefits over using the Python code (it might actually be slower — yet to be benchmarked). However, it can be used as a starting point for further optimization if you decide to rewrite performance-critical parts of your codebase in pure Rust. + +## Overview + +The workspace contains these packages: + +- **[pyo3_bindgen](pyo3_bindgen):** Public API for generation of bindings (in `build.rs` scripts or via procedural macros if enabled) +- **[pyo3_bindgen_cli](pyo3_bindgen_cli):** CLI tool for generation of bindings via `pyo3_bindgen` executable +- **[pyo3_bindgen_engine](pyo3_bindgen_engine):** The underlying engine for generation of bindings +- **[pyo3_bindgen_macros](pyo3_bindgen_macros):** \[Experimental\] Procedural macros for in-place generation + +## Instructions + +Add `pyo3` as a dependency and `pyo3_bindgen` as a build dependency to your [`Cargo.toml`](https://doc.rust-lang.org/cargo/reference/manifest.html) manifest (`auto-initialize` feature of `pyo3` is optional and shown here for your convenience). + +```toml +[dependencies] +pyo3 = { version = "0.20", features = ["auto-initialize"] } + +[build-dependencies] +pyo3_bindgen = { version = "0.1" } +``` + +### Option 1: Build script + +Create a [`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html) script in the root of your crate that generates bindings to the `target_module` Python module. + +```rs +// build.rs + +fn main() { + // Generate Rust bindings to the Python module + pyo3_bindgen::build_bindings( + "target_module", + std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("bindings.rs"), + ) + .unwrap(); +} +``` + +Afterwards, include the generated bindings anywhere in your crate. + +```rs +#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +pub mod target_module { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} +``` + +### Option 2: CLI tool + +Install the `pyo3_bindgen` executable with `cargo`. + +```bash +cargo install --locked pyo3_bindgen_cli +``` + +Afterwards, run the `pyo3_bindgen` executable while passing the name of the target Python module. + +```bash +# Pass `--help` to show the usage and available options +pyo3_bindgen -m target_module -o bindings.rs +``` + +### Option 3 \[Experimental\]: Procedural macros + +> **Note:** This feature is experimental and will probably fail in many cases. It is recommended to use build scripts instead. + +Enable the `macros` feature of `pyo3_bindgen`. + +```toml +[build-dependencies] +pyo3_bindgen = { version = "0.1", features = ["macros"] } +``` + +Then, you can call the `import_python!` macro anywhere in your crate. + +```rs +#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +pub(crate) mod target_module { + pyo3_bindgen::import_python!("target_module"); +} +``` + +## Status + +This project is in early development, and as such, the API of the generated bindings is not yet stable. + +- Not all Python types are mapped to their Rust equivalents yet. Especially the support for mapping types of module-wide classes for which bindings are generated is also still missing. For this reason, a lot of boilerplate might be currently required when using the generated bindings (e.g. `let typed_value: target_module::Class = any_value.extract()?;`). +- The binding generation is primarily designed to be used inside build scripts or via procedural macros. Therefore, the performance of the codegen process is [benchmarked](./pyo3_bindgen_engine/benches/bindgen.rs) to understand the potential impact on build times. Surprisingly, even the initial unoptimized version of the engine is able to process the entire `numpy` 1.26.3 in ~300 ms on a *modern* laptop while generating 166k lines of formatted Rust code (line count includes documentation). Adding more features might increase this time, but there is also plenty of room for optimization in the current naive implementation. +- The generation of bindings should never panic as long as the target Python module can be successfully imported. If it does, it is a bug resulting from an unexpected edge-case Python module structure or an unforeseen combination of enabled PyO3 features. +- Although implemented, the procedural macros might not work in all cases - especially when some PyO3 features are enabled. In most cases, PyO3 fails to import the target Python module when used from within a `proc_macro` crate. Therefore, it is recommended to use build scripts instead for now. +- The code will be refactored and cleaned up in the upcoming releases. The current implementation is a result of a very quick prototype that was built to test the feasibility of the idea. + +Please [report](https://github.com/AndrejOrsula/pyo3_bindgen/issues/new) any issues that you might encounter. Contributions are more than welcome! If you are looking for a place to start, consider searching for `TODO` comments in the codebase. + +## License + +This project is dual-licensed to be compatible with the Rust project, under either the [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE) licenses. + +## Contributing + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..1d316cb --- /dev/null +++ b/deny.toml @@ -0,0 +1,53 @@ +# `cargo deny` is only intended to run these targets for this project +targets = [ + { triple = "aarch64-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-musl" }, +] + +# Considered when running `cargo deny check advisories` +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +notice = "deny" +unmaintained = "warn" +unsound = "deny" +vulnerability = "deny" +yanked = "deny" +ignore = [] + +# Considered when running `cargo deny check licenses` +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +allow-osi-fsf-free = "neither" +copyleft = "deny" +unlicensed = "deny" +private = { ignore = true } +confidence-threshold = 0.925 +allow = [ + "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html + "Apache-2.0", # https://spdx.org/licenses/Apache-2.0.html + "BSD-3-Clause", # https://spdx.org/licenses/BSD-3-Clause.html + "ISC", # https://spdx.org/licenses/ISC.html + "MIT", # https://spdx.org/licenses/MIT.html + "MPL-2.0", # https://spdx.org/licenses/MPL-2.0.html + "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html +] +exceptions = [] + +# Considered when running `cargo deny check bans` +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +multiple-versions = "warn" +wildcards = "allow" +deny = [] +skip = [] +skip-tree = [] + +# Considered when running `cargo deny check sources` +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +[sources.allow-org] +github = ["AndrejOrsula"] diff --git a/pyo3_bindgen/Cargo.toml b/pyo3_bindgen/Cargo.toml new file mode 100644 index 0000000..c33215d --- /dev/null +++ b/pyo3_bindgen/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "pyo3_bindgen" +authors.workspace = true +categories.workspace = true +description = "Automatic generation of Rust bindings to Python modules" +edition.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +pyo3_bindgen_engine = { workspace = true } +pyo3_bindgen_macros = { workspace = true, optional = true } + +[features] +default = ["macros"] +macros = ["pyo3_bindgen_macros"] + +[lib] +name = "pyo3_bindgen" +path = "src/lib.rs" +crate-type = ["rlib"] diff --git a/pyo3_bindgen/src/lib.rs b/pyo3_bindgen/src/lib.rs new file mode 100644 index 0000000..19d8b7c --- /dev/null +++ b/pyo3_bindgen/src/lib.rs @@ -0,0 +1,82 @@ +//! Public API library for automatic generation of Rust FFI bindings to Python modules. +//! +//! ## Instructions +//! +//! Add `pyo3` as a dependency and `pyo3_bindgen` as a build dependency to your [`Cargo.toml`](https://doc.rust-lang.org/cargo/reference/manifest.html) manifest (`auto-initialize` feature of `pyo3` is optional and shown here for your convenience). +//! +//! ```toml +//! [dependencies] +//! pyo3 = { version = "0.20", features = ["auto-initialize"] } +//! +//! [build-dependencies] +//! pyo3_bindgen = { version = "0.1" } +//! ``` +//! +//! ### Option 1: Build script +//! +//! Create a [`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html) script in the root of your crate that generates bindings to the `target_module` Python module. +//! +//! ```rs +//! // build.rs +//! +//! fn main() { +//! // Generate Rust bindings to the Python module +//! pyo3_bindgen::build_bindings( +//! "target_module", +//! std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("bindings.rs"), +//! ) +//! .unwrap(); +//! } +//! ``` +//! +//! Afterwards, include the generated bindings anywhere in your crate. +//! +//! ```rs +//! #[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +//! pub mod target_module { +//! include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +//! } +//! ``` +//! +//! ### Option 2: CLI tool +//! +//! Install the `pyo3_bindgen` executable with `cargo`. +//! +//! ```bash +//! cargo install --locked pyo3_bindgen_cli +//! ``` +//! +//! Afterwards, run the `pyo3_bindgen` executable while passing the name of the target Python module. +//! +//! ```bash +//! # Pass `--help` to show the usage and available options +//! pyo3_bindgen -m target_module -o bindings.rs +//! ``` +//! +//! ### Option 3 \[Experimental\]: Procedural macros +//! +//! > **Note:** This feature is experimental and will probably fail in many cases. It is recommended to use build scripts instead. +//! +//! Enable the `macros` feature of `pyo3_bindgen`. +//! +//! ```toml +//! [build-dependencies] +//! pyo3_bindgen = { version = "0.1", features = ["macros"] } +//! ``` +//! +//! Then, you can call the `import_python!` macro anywhere in your crate. +//! +//! ```rs +//! #[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +//! pub(crate) mod target_module { +//! pyo3_bindgen::import_python!("target_module"); +//! } +//! ``` + +pub use pyo3_bindgen_engine::{ + self as engine, build_bindings, generate_bindings, generate_bindings_for_module, + generate_bindings_from_str, +}; + +#[cfg(feature = "macros")] +pub use pyo3_bindgen_macros::{self as macros, import_python}; diff --git a/pyo3_bindgen_cli/Cargo.toml b/pyo3_bindgen_cli/Cargo.toml new file mode 100644 index 0000000..558bc69 --- /dev/null +++ b/pyo3_bindgen_cli/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pyo3_bindgen_cli" +authors.workspace = true +categories.workspace = true +description = "CLI tool for automatic generation of Rust bindings to Python modules" +edition.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +clap = { workspace = true } +prettyplease = { workspace = true } +pyo3_bindgen = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +assert_cmd = { workspace = true } +predicates = { workspace = true } + +[[bin]] +name = "pyo3_bindgen" +path = "src/main.rs" +doc = false diff --git a/pyo3_bindgen_cli/src/main.rs b/pyo3_bindgen_cli/src/main.rs new file mode 100644 index 0000000..86e0306 --- /dev/null +++ b/pyo3_bindgen_cli/src/main.rs @@ -0,0 +1,86 @@ +//! CLI tool for automatic generation of Rust FFI bindings to Python modules. + +use clap::Parser; + +fn main() { + // Parse the CLI arguments + let args = Args::parse(); + + // Generate the bindings for the module specified by the `--module-name` argument + let bindings = pyo3_bindgen::generate_bindings(&args.module_name).unwrap_or_else(|_| { + panic!( + "Failed to generate bindings for module: {}", + args.module_name + ) + }); + + let bindings = prettyplease::unparse(&syn::parse2(bindings).unwrap()); + + if let Some(output) = args.output { + // Write the bindings to a file if the `--output` argument is provided + if let Some(parent) = output.parent() { + std::fs::create_dir_all(parent).unwrap_or_else(|_| { + panic!("Failed to create output directory: {}", parent.display()) + }); + } + std::fs::write(&output, &bindings) + .unwrap_or_else(|_| panic!("Failed to write to file: {}", output.display())); + } else { + // Otherwise, print the bindings to STDOUT + println!("{bindings}"); + } +} + +/// Arguments for the CLI tool +#[derive(Parser)] +#[command(author, version, about)] +struct Args { + #[arg(short, long)] + /// Name of the Python module for which to generate the bindings + pub module_name: String, + #[arg(short, long)] + /// Name of the output file to which to write the bindings [default: STDOUT] + pub output: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser_all() { + // Arrange + let input = ["", "-m", "pip", "--output", "bindings.rs"]; + + // Act + let args = Args::parse_from(input); + + // Assert + assert_eq!(args.module_name, "pip"); + assert_eq!(args.output, Some("bindings.rs".into())); + } + + #[test] + fn test_parser_short() { + // Arrange + let input = ["", "-m", "numpy"]; + + // Act + let args = Args::parse_from(input); + + // Assert + assert_eq!(args.module_name, "numpy"); + } + + #[test] + fn test_parser_long() { + // Arrange + let input = ["", "--module-name", "setuptools"]; + + // Act + let args = Args::parse_from(input); + + // Assert + assert_eq!(args.module_name, "setuptools"); + } +} diff --git a/pyo3_bindgen_cli/tests/cli.rs b/pyo3_bindgen_cli/tests/cli.rs new file mode 100644 index 0000000..221034f --- /dev/null +++ b/pyo3_bindgen_cli/tests/cli.rs @@ -0,0 +1,64 @@ +#[cfg(target_arch = "x86_64")] +mod test_cli { + use assert_cmd::Command; + use predicates::prelude::*; + + const BIN_NAME: &str = "pyo3_bindgen"; + + #[test] + fn test_cli_help() { + // Arrange + let mut cmd = Command::cargo_bin(BIN_NAME).unwrap(); + + // Act + let assert = cmd.arg("-h").assert(); + + // Assert + assert.success().stdout( + predicate::str::contains(format!("Usage: {BIN_NAME}")) + .and(predicate::str::contains("Options:")) + .and(predicate::str::contains("--module-name ")) + .and(predicate::str::contains("--output ")), + ); + } + + #[test] + fn test_cli_default() { + // Arrange + let mut cmd = Command::cargo_bin(BIN_NAME).unwrap(); + + // Act + let assert = cmd.assert(); + + // Assert + assert.failure().stderr( + predicate::str::contains("error: the following required arguments") + .and(predicate::str::contains("--module-name ")) + .and(predicate::str::contains(format!("Usage: {BIN_NAME}"))), + ); + } + + #[test] + fn test_cli_bindgen_os() { + // Arrange + let mut cmd = Command::cargo_bin(BIN_NAME).unwrap(); + + // Act + let assert = cmd.arg("-m").arg("os").assert(); + + // Assert + assert.success(); + } + + #[test] + fn test_cli_bindgen_sys() { + // Arrange + let mut cmd = Command::cargo_bin(BIN_NAME).unwrap(); + + // Act + let assert = cmd.arg("-m").arg("sys").assert(); + + // Assert + assert.success(); + } +} diff --git a/pyo3_bindgen_engine/Cargo.toml b/pyo3_bindgen_engine/Cargo.toml new file mode 100644 index 0000000..99275c3 --- /dev/null +++ b/pyo3_bindgen_engine/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "pyo3_bindgen_engine" +authors.workspace = true +categories.workspace = true +description = "Engine behind automatic generation of Rust bindings to Python modules" +edition.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +itertools = { workspace = true } +proc-macro2 = { workspace = true } +pyo3 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +indoc = { workspace = true } +prettyplease = { workspace = true } + +[build-dependencies] +pyo3-build-config = { workspace = true } + +[lib] +name = "pyo3_bindgen_engine" +path = "src/lib.rs" +crate-type = ["rlib"] + +[[bench]] +name = "bindgen" +harness = false diff --git a/pyo3_bindgen_engine/benches/bindgen.rs b/pyo3_bindgen_engine/benches/bindgen.rs new file mode 100644 index 0000000..1383b99 --- /dev/null +++ b/pyo3_bindgen_engine/benches/bindgen.rs @@ -0,0 +1,107 @@ +macro_rules! bench_bindgen_from_str { + { + |$criterion:ident| $(,)? + $bench_name:ident $(,)? + $(py)?$(python)? $(:)? $code_py:literal $(,)? + } => { + { + const CODE_PY: &str = indoc::indoc! { $code_py }; + $criterion.bench_function(stringify!($bench_name), |b| { + b.iter(|| { + pyo3_bindgen_engine::generate_bindings_from_str( + criterion::black_box(CODE_PY), + criterion::black_box(concat!("bench_mod_", stringify!($bench_name))), + ) + .unwrap() + }); + }); + } + }; +} + +macro_rules! try_bench_bindgen_for_module { + { + |$py:ident, $criterion:ident| $(,)? + $(module)? $(:)? $module_name:literal $(,)? + } => { + if let Ok(module) = $py.import($module_name) { + $criterion.bench_function(concat!("bench_bindgen_module_", $module_name), |b| { + b.iter(|| { + pyo3_bindgen_engine::generate_bindings_for_module( + criterion::black_box($py), + criterion::black_box(module), + ) + .unwrap() + }); + }); + } + }; +} + +fn criterion_benchmark(crit: &mut criterion::Criterion) { + let mut group_from_str = crit.benchmark_group("generate_bindings_from_str"); + group_from_str + .warm_up_time(std::time::Duration::from_millis(250)) + .sample_size(100); + bench_bindgen_from_str! { + |group_from_str| + bench_bindgen_attribute + py: r#" + t_const_float: float = 0.42 + "# + } + bench_bindgen_from_str! { + |group_from_str| + bench_bindgen_function + py: r#" + def t_fn(t_arg1: str) -> int: + """t_docs""" + ... + "# + } + bench_bindgen_from_str! { + |group_from_str| + bench_bindgen_class + py: r#" + from typing import Dict, Optional + class t_class: + """t_docs""" + def __init__(self, t_arg1: str, t_arg2: Optional[int] = None): + """t_docs_init""" + ... + def t_method(self, t_arg1: Dict[str, int], **kwargs): + """t_docs_method""" + ... + @property + def t_prop(self) -> int: + ... + @t_prop.setter + def t_prop(self, value: int): + ... + "# + } + group_from_str.finish(); + + let mut group_for_module = crit.benchmark_group("generate_bindings_for_module"); + group_for_module + .warm_up_time(std::time::Duration::from_secs(2)) + .sample_size(10); + pyo3::Python::with_gil(|py| { + try_bench_bindgen_for_module! { + |py, group_for_module| + module: "os" + } + try_bench_bindgen_for_module! { + |py, group_for_module| + module: "sys" + } + try_bench_bindgen_for_module! { + |py, group_for_module| + module: "numpy" + } + }); + group_for_module.finish(); +} + +criterion::criterion_group!(benches, criterion_benchmark); +criterion::criterion_main!(benches); diff --git a/pyo3_bindgen_engine/build.rs b/pyo3_bindgen_engine/build.rs new file mode 100644 index 0000000..16dc4f4 --- /dev/null +++ b/pyo3_bindgen_engine/build.rs @@ -0,0 +1,4 @@ +fn main() { + // Expose #[cfg] flags of pyo3 + pyo3_build_config::use_pyo3_cfgs(); +} diff --git a/pyo3_bindgen_engine/src/bindgen.rs b/pyo3_bindgen_engine/src/bindgen.rs new file mode 100644 index 0000000..2542b4a --- /dev/null +++ b/pyo3_bindgen_engine/src/bindgen.rs @@ -0,0 +1,127 @@ +//! Module for handling the binding generation process. + +pub mod attribute; +pub mod class; +pub mod function; +pub mod module; + +pub use attribute::bind_attribute; +pub use class::bind_class; +pub use function::bind_function; +pub use module::{bind_module, bind_reexport}; + +/// Generate Rust bindings to a Python module specified by its name. Generating bindings to +/// submodules such as `os.path` is also supported as long as the module can be directly imported +/// from the Python interpreter via `import os.path`. +/// +/// # Arguments +/// +/// * `module_name` - Name of the Python module to generate bindings for. +/// +/// # Returns +/// +/// `Result` containing the generated bindings as a `proc_macro2::TokenStream` on success, or a +/// `pyo3::PyErr` on failure. +/// +/// # Example +/// +/// ``` +/// // use pyo3_bindgen::generate_bindings; +/// use pyo3_bindgen_engine::generate_bindings; +/// +/// fn main() -> Result<(), pyo3::PyErr> { +/// let bindings: proc_macro2::TokenStream = generate_bindings("os")?; +/// Ok(()) +/// } +/// ``` +pub fn generate_bindings(module_name: &str) -> Result { + #[cfg(not(PyPy))] + pyo3::prepare_freethreaded_python(); + + pyo3::Python::with_gil(|py| { + let module = py.import(module_name)?; + generate_bindings_for_module(py, module) + }) +} + +/// Generate Rust bindings to an instance of `pyo3::types::PyModule` Python module. +/// +/// # Arguments +/// +/// * `py` - Python interpreter instance. +/// * `module` - Python module to generate bindings for. +/// +/// # Returns +/// +/// `Result` containing the generated bindings as a `proc_macro2::TokenStream` on success, or a +/// `pyo3::PyErr` on failure. +/// +/// # Example +/// +/// ``` +/// // use pyo3_bindgen::generate_bindings_for_module; +/// use pyo3_bindgen_engine::generate_bindings_for_module; +/// +/// fn main() -> Result<(), pyo3::PyErr> { +/// pyo3::prepare_freethreaded_python(); +/// let bindings: proc_macro2::TokenStream = pyo3::Python::with_gil(|py| { +/// let module = py.import("os")?; +/// generate_bindings_for_module(py, module) +/// })?; +/// Ok(()) +/// } +/// ``` +pub fn generate_bindings_for_module( + py: pyo3::Python, + module: &pyo3::types::PyModule, +) -> Result { + bind_module(py, module, module, &mut std::collections::HashSet::new()) +} + +/// Generate Rust bindings to a Python module specified by its `source_code`. The module will be +/// named `new_module_name` in the generated bindings. However, the generated bindings might not +/// be immediately functional if the module represented by its `source_code` is not a known Python +/// module in the current Python interpreter. +/// +/// # Arguments +/// +/// * `source_code` - Source code of the Python module to generate bindings for. +/// * `new_module_name` - Name of the Python module to generate bindings for. +/// +/// # Returns +/// +/// `Result` containing the generated bindings as a `proc_macro2::TokenStream` on success, or a +/// `pyo3::PyErr` on failure. +/// +/// # Example +/// +/// ``` +/// // use pyo3_bindgen::generate_bindings_from_str; +/// use pyo3_bindgen_engine::generate_bindings_from_str; +/// +/// fn main() -> Result<(), pyo3::PyErr> { +/// const PYTHON_SOURCE_CODE: &str = r#" +/// def string_length(string: str) -> int: +/// return len(string) +/// "#; +/// let bindings = generate_bindings_from_str(PYTHON_SOURCE_CODE, "utils")?; +/// Ok(()) +/// } +/// ``` +pub fn generate_bindings_from_str( + source_code: &str, + new_module_name: &str, +) -> Result { + #[cfg(not(PyPy))] + pyo3::prepare_freethreaded_python(); + + pyo3::Python::with_gil(|py| { + let module = pyo3::types::PyModule::from_code( + py, + source_code, + &format!("{new_module_name}/__init__.py"), + new_module_name, + )?; + generate_bindings_for_module(py, module) + }) +} diff --git a/pyo3_bindgen_engine/src/bindgen/attribute.rs b/pyo3_bindgen_engine/src/bindgen/attribute.rs new file mode 100644 index 0000000..fa8ab4d --- /dev/null +++ b/pyo3_bindgen_engine/src/bindgen/attribute.rs @@ -0,0 +1,131 @@ +use crate::types::map_attr_type; + +/// Generate Rust bindings to a Python attribute. The attribute can be a standalone +/// attribute or a property of a class. +pub fn bind_attribute( + py: pyo3::Python, + module_name: Option<&str>, + name: &str, + attr: &pyo3::PyAny, + attr_type: &pyo3::PyAny, +) -> Result { + let mut token_stream = proc_macro2::TokenStream::new(); + + let mut has_setter = true; + let mut getter_type = attr_type; + let mut setter_type = attr_type; + let getter_doc = py.None(); + let mut getter_doc = getter_doc.as_ref(py); + let setter_doc = py.None(); + let mut setter_doc = setter_doc.as_ref(py); + + // Check if the attribute has a getter and setter (is a property) + if let Ok(getter) = attr.getattr("fget") { + let inspect = py.import("inspect")?; + let signature = inspect.call_method1("signature", (getter,))?; + let empty_return_annotation = signature.getattr("empty")?; + let return_annotation = signature.getattr("return_annotation")?; + if !return_annotation.is(empty_return_annotation) { + getter_type = return_annotation; + } + if let Ok(doc) = getter.getattr("__doc__") { + getter_doc = doc; + } + has_setter = false; + } + if let Ok(setter) = attr.getattr("fset") { + if !setter.is_none() { + let inspect = py.import("inspect")?; + let signature = inspect.call_method1("signature", (setter,))?; + let empty_return_annotation = signature.getattr("empty")?; + let value_annotation = signature + .getattr("parameters")? + .call_method0("values")? + .iter()? + .last() + .unwrap()? + .getattr("annotation")?; + if !value_annotation.is(empty_return_annotation) { + setter_type = value_annotation; + } + if let Ok(doc) = setter.getattr("__doc__") { + setter_doc = doc; + } + has_setter = true; + } + } + + let mut getter_doc = getter_doc.to_string(); + if getter_doc == "None" || getter_doc.is_empty() { + getter_doc = format!("Getter for the `{name}` attribute"); + }; + + let mut setter_doc = setter_doc.to_string(); + if setter_doc == "None" || setter_doc.is_empty() { + setter_doc = format!("Setter for the `{name}` attribute"); + }; + + let getter_ident = if syn::parse_str::(name).is_ok() { + quote::format_ident!("{}", name) + } else { + quote::format_ident!("r#{}", name) + }; + let setter_ident = quote::format_ident!("set_{}", name); + + let getter_type = map_attr_type(getter_type, true)?; + let setter_type = map_attr_type(setter_type, false)?; + + if let Some(module_name) = module_name { + token_stream.extend(quote::quote! { + #[doc = #getter_doc] + pub fn #getter_ident<'py>( + py: ::pyo3::marker::Python<'py>, + ) -> ::pyo3::PyResult<#getter_type> { + py.import(::pyo3::intern!(py, #module_name))? + .getattr(::pyo3::intern!(py, #name))? + .extract() + } + }); + if has_setter { + token_stream.extend(quote::quote! { + #[doc = #setter_doc] + pub fn #setter_ident<'py>( + py: ::pyo3::marker::Python<'py>, + value: #setter_type, + ) -> ::pyo3::PyResult<()> { + py.import(::pyo3::intern!(py, #module_name))? + .setattr(::pyo3::intern!(py, #name), value)?; + Ok(()) + } + }); + } + } else { + token_stream.extend(quote::quote! { + #[doc = #getter_doc] + pub fn #getter_ident<'py>( + &'py self, + py: ::pyo3::marker::Python<'py>, + ) -> ::pyo3::PyResult<#getter_type> { + self.as_ref(py) + .getattr(::pyo3::intern!(py, #name))? + .extract() + } + }); + if has_setter { + token_stream.extend(quote::quote! { + #[doc = #setter_doc] + pub fn #setter_ident<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + value: #setter_type, + ) -> ::pyo3::PyResult<()> { + self.as_ref(py) + .setattr(::pyo3::intern!(py, #name), value)?; + Ok(()) + } + }); + } + } + + Ok(token_stream) +} diff --git a/pyo3_bindgen_engine/src/bindgen/class.rs b/pyo3_bindgen_engine/src/bindgen/class.rs new file mode 100644 index 0000000..8980c4b --- /dev/null +++ b/pyo3_bindgen_engine/src/bindgen/class.rs @@ -0,0 +1,197 @@ +use crate::bindgen::{bind_attribute, bind_function}; + +/// Generate Rust bindings to a Python class with all its methods and attributes (properties). +/// This function will call itself recursively to generate bindings to all nested classes. +pub fn bind_class( + py: pyo3::Python, + root_module: &pyo3::types::PyModule, + class: &pyo3::types::PyType, +) -> Result { + let inspect = py.import("inspect")?; + + // Extract the names of the modules + let root_module_name = root_module.name()?; + let full_class_name = class.name()?; + let class_name: &str = full_class_name.split('.').last().unwrap(); + + // Create the Rust class identifier (raw string if it is a keyword) + let class_ident = if syn::parse_str::(class_name).is_ok() { + quote::format_ident!("{class_name}") + } else { + quote::format_ident!("r#{class_name}") + }; + + let mut fn_names = Vec::new(); + + // Iterate over all attributes of the module while updating the token stream + let mut impl_token_stream = proc_macro2::TokenStream::new(); + class + .dir() + .iter() + .map(|name| { + let name = name.str().unwrap().to_str().unwrap(); + let attr = class.getattr(name).unwrap(); + let attr_type = attr.get_type(); + (name, attr, attr_type) + }) + .filter(|&(_, _, attr_type)| { + // Skip builtin functions + !attr_type + .is_subclass_of::() + .unwrap_or(false) + }) + .filter(|&(name, _, _)| { + // Skip private attributes (except for __init__ and __call__) + !name.starts_with('_') || name == "__init__" || name == "__call__" + }) + .filter(|(_, attr, attr_type)| { + // Skip typing attributes + !attr + .getattr("__module__") + .is_ok_and(|module| module.to_string().contains("typing")) + && !attr_type.to_string().contains("typing") + }) + .filter(|(_, attr, _)| { + // Skip __future__ attributes + !attr + .getattr("__module__") + .is_ok_and(|module| module.to_string().contains("__future__")) + }) + .filter(|&(_, attr, _)| { + // Skip classes and functions that are not part of the package + // However, this should keep instances of classes and builtins even if they are builtins or from other packages + if let Ok(module) = attr.getattr("__module__") { + if module.to_string().starts_with(root_module_name) { + true + } else { + !(inspect + .call_method1("isclass", (attr,)) + .unwrap() + .is_true() + .unwrap() + || inspect + .call_method1("isfunction", (attr,)) + .unwrap() + .is_true() + .unwrap()) + } + } else { + true + } + }) + .filter(|&(_, attr, attr_type)| { + // Skip external modules + if attr_type + .is_subclass_of::() + .unwrap_or(false) + { + let is_submodule = attr + .getattr("__package__") + .is_ok_and(|package| package.to_string().starts_with(root_module_name)); + is_submodule + } else { + true + } + }) + .for_each(|(name, attr, attr_type)| { + let is_internal = attr + .getattr("__module__") + .unwrap_or(pyo3::types::PyString::new(py, "")) + .to_string() + .starts_with(root_module_name); + let is_reexport = is_internal + && attr + .getattr("__module__") + .unwrap_or(pyo3::types::PyString::new(py, "")) + .to_string() + .ne(full_class_name); + + let is_class = attr_type + .is_subclass_of::() + .unwrap_or(false); + + let is_function = inspect + .call_method1("isfunction", (attr,)) + .unwrap() + .is_true() + .unwrap() + || inspect + .call_method1("ismethod", (attr,)) + .unwrap() + .is_true() + .unwrap(); + + // Make sure that only one of the three is true + debug_assert!(![is_class, is_function].iter().all(|&v| v)); + + if is_class && !is_reexport { + impl_token_stream.extend(bind_class(py, root_module, attr.downcast().unwrap())); + } else if is_function { + fn_names.push(name.to_string()); + impl_token_stream.extend(bind_function(py, "", name, attr)); + } else if !name.starts_with('_') { + impl_token_stream.extend(bind_attribute(py, None, name, attr, attr_type)); + } + }); + + // Add new and call aliases (currently a reimplemented versions of the function) + if fn_names.contains(&"__init__".to_string()) && !fn_names.contains(&"new".to_string()) { + impl_token_stream.extend(bind_function(py, "", "new", class.getattr("__init__")?)); + } + if fn_names.contains(&"__call__".to_string()) && !fn_names.contains(&"call".to_string()) { + impl_token_stream.extend(bind_function(py, "", "call", class.getattr("__call__")?)); + } + + let mut doc = class.getattr("__doc__")?.to_string(); + if doc == "None" { + doc = String::new(); + }; + + Ok(quote::quote! { + #[doc = #doc] + #[repr(transparent)] + #[derive(Clone, Debug)] + pub struct #class_ident(pub ::pyo3::PyObject); + #[automatically_derived] + impl ::std::ops::Deref for #class_ident { + type Target = ::pyo3::PyObject; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + #[automatically_derived] + impl ::std::ops::DerefMut for #class_ident { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + #[automatically_derived] + impl<'py> ::pyo3::FromPyObject<'py> for #class_ident { + fn extract(value: &'py ::pyo3::PyAny) -> ::pyo3::PyResult { + Ok(Self(value.into())) + } + } + #[automatically_derived] + impl ::pyo3::ToPyObject for #class_ident { + fn to_object<'py>(&'py self, py: ::pyo3::Python<'py>) -> ::pyo3::PyObject { + self.as_ref(py).to_object(py) + } + } + #[automatically_derived] + impl From<::pyo3::PyObject> for #class_ident { + fn from(value: ::pyo3::PyObject) -> Self { + Self(value) + } + } + #[automatically_derived] + impl<'py> From<&'py ::pyo3::PyAny> for #class_ident { + fn from(value: &'py ::pyo3::PyAny) -> Self { + Self(value.into()) + } + } + #[automatically_derived] + impl #class_ident { + #impl_token_stream + } + }) +} diff --git a/pyo3_bindgen_engine/src/bindgen/function.rs b/pyo3_bindgen_engine/src/bindgen/function.rs new file mode 100644 index 0000000..825c51a --- /dev/null +++ b/pyo3_bindgen_engine/src/bindgen/function.rs @@ -0,0 +1,253 @@ +use itertools::Itertools; +use pyo3::PyTypeInfo; + +use crate::types::map_attr_type; + +/// Generate Rust bindings to a Python function. The function can be a standalone function or a +/// method of a class. +pub fn bind_function( + py: pyo3::Python, + module_name: &str, + name: &str, + function: &pyo3::PyAny, +) -> Result { + let inspect = py.import("inspect")?; + + let signature = inspect.call_method1("signature", (function,))?; + + let empty_return_annotation = signature.getattr("empty")?; + + let parameters = signature.getattr("parameters")?; + let return_annotation = signature.getattr("return_annotation")?; + + let return_annotation = if return_annotation.is(empty_return_annotation) { + None + } else { + Some(return_annotation) + }; + + let mut positional_args_idents = Vec::new(); + let mut keyword_args_idents = Vec::new(); + let mut keyword_args_names = Vec::new(); + let mut var_positional_ident = None; + let mut var_keyword_ident = None; + + let parameters = parameters + .call_method0("values")? + .iter()? + .map(|parameter| { + let parameter = parameter.unwrap(); + + let empty_param_annotation = parameter.getattr("empty").unwrap(); + + let param_name = parameter.getattr("name").unwrap().to_string(); + + let param_default = parameter.getattr("default").unwrap(); + let param_annotation = parameter.getattr("annotation").unwrap(); + let param_kind = parameter.getattr("kind").unwrap(); + + let param_annotation = if param_annotation.is(empty_param_annotation) { + None + } else { + Some(param_annotation) + }; + let param_default = if param_default.is(empty_param_annotation) { + None + } else { + Some(param_default) + }; + // TODO: Turn into enum or process in-place + // TODO: Fully support positional-only parameters + let param_kind = match param_kind.extract::().unwrap() { + 0 => "POSITIONAL_ONLY", + 1 => "POSITIONAL_OR_KEYWORD", + 2 => "VAR_POSITIONAL", // args + 3 => "KEYWORD_ONLY", + 4 => "VAR_KEYWORD", // kwargs + _ => unreachable!(), + }; + + if param_name != "self" { + match param_kind { + "POSITIONAL_ONLY" | "POSITIONAL_OR_KEYWORD" => { + positional_args_idents.push( + if syn::parse_str::(¶m_name).is_ok() { + quote::format_ident!("{}", param_name) + } else { + quote::format_ident!("r#{}", param_name) + }, + ); + } + "KEYWORD_ONLY" => { + keyword_args_idents.push( + if syn::parse_str::(¶m_name).is_ok() { + quote::format_ident!("{}", param_name) + } else { + quote::format_ident!("r#{}", param_name) + }, + ); + keyword_args_names.push(param_name.clone()); + } + "VAR_POSITIONAL" => { + var_positional_ident = + Some(if syn::parse_str::(¶m_name).is_ok() { + quote::format_ident!("{}", param_name) + } else { + quote::format_ident!("r#{}", param_name) + }); + positional_args_idents.push( + if syn::parse_str::(¶m_name).is_ok() { + quote::format_ident!("{}", param_name) + } else { + quote::format_ident!("r#{}", param_name) + }, + ); + } + "VAR_KEYWORD" => { + var_keyword_ident = + Some(if syn::parse_str::(¶m_name).is_ok() { + quote::format_ident!("{}", param_name) + } else { + quote::format_ident!("r#{}", param_name) + }); + } + _ => unreachable!(), + } + } + + let param_annotation = match param_kind { + "VAR_POSITIONAL" => Some(pyo3::types::PyTuple::type_object(py).downcast().unwrap()), + "VAR_KEYWORD" => Some(pyo3::types::PyDict::type_object(py).downcast().unwrap()), + _ => param_annotation, + }; + + (param_name, param_annotation, param_default, param_kind) + }) + .collect_vec(); + + let function_ident = if syn::parse_str::(name).is_ok() { + quote::format_ident!("{}", name) + } else { + quote::format_ident!("r#{}", name) + }; + let function_name = function.getattr("__name__")?.to_string(); + + // Check if `self` is the first parameter + let has_self_param = parameters + .iter() + .any(|(param_name, _, _, _)| param_name == "self"); + + let param_idents = parameters + .iter() + .skip(usize::from(has_self_param)) + .map(|(param_name, _, _, _)| { + if syn::parse_str::(param_name).is_ok() { + quote::format_ident!("{}", param_name) + } else { + quote::format_ident!("r#{}", param_name) + } + }) + .collect_vec(); + let pynone = py.None(); + let pynone = pynone.as_ref(py); + let param_types = parameters + .iter() + .skip(usize::from(has_self_param)) + .map(|(_, param_annotation, _, _)| { + map_attr_type(param_annotation.unwrap_or_else(|| pynone), false).unwrap() + }) + .collect_vec(); + let return_annotation = map_attr_type(return_annotation.unwrap_or(pynone), true)?; + + let mut doc = function.getattr("__doc__")?.to_string(); + if doc == "None" { + doc = String::new(); + }; + + // TODO: Use `call_method0` and `call_method1`` where appropriate + Ok(if has_self_param { + if let Some(var_keyword_ident) = var_keyword_ident { + quote::quote! { + #[doc = #doc] + pub fn #function_ident<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + #(#param_idents: #param_types),* + ) -> ::pyo3::PyResult<#return_annotation> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + #({ + let #positional_args_idents: ::pyo3::PyObject = #positional_args_idents.into_py(py); + #positional_args_idents + },)* + ); + let __internal_kwargs = #var_keyword_ident; + #(__internal_kwargs.set_item(::pyo3::intern!(py, #keyword_args_names), #keyword_args_idents)?;)* + self.as_ref(py).call_method(::pyo3::intern!(py, #function_name), __internal_args, Some(__internal_kwargs))?.extract() + } + } + } else { + quote::quote! { + #[doc = #doc] + pub fn #function_ident<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + #(#param_idents: #param_types),* + ) -> ::pyo3::PyResult<#return_annotation> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + #({ + let #positional_args_idents: ::pyo3::PyObject = #positional_args_idents.into_py(py); + #positional_args_idents + },)* + ); + let __internal_kwargs = ::pyo3::types::PyDict::new(py); + #(__internal_kwargs.set_item(::pyo3::intern!(py, #keyword_args_names), #keyword_args_idents)?;)* + self.as_ref(py).call_method(::pyo3::intern!(py, #function_name), __internal_args, Some(__internal_kwargs))?.extract() + } + } + } + } else if let Some(var_keyword_ident) = var_keyword_ident { + quote::quote! { + #[doc = #doc] + pub fn #function_ident<'py>( + py: ::pyo3::marker::Python<'py>, + #(#param_idents: #param_types),* + ) -> ::pyo3::PyResult<#return_annotation> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + #({ + let #positional_args_idents: ::pyo3::PyObject = #positional_args_idents.into_py(py); + #positional_args_idents + },)* + ); + let __internal_kwargs = #var_keyword_ident; + #(__internal_kwargs.set_item(::pyo3::intern!(py, #keyword_args_names), #keyword_args_idents)?;)* + py.import(::pyo3::intern!(py, #module_name))?.call_method(::pyo3::intern!(py, #function_name), __internal_args, Some(__internal_kwargs))?.extract() + } + } + } else { + quote::quote! { + #[doc = #doc] + pub fn #function_ident<'py>( + py: ::pyo3::marker::Python<'py>, + #(#param_idents: #param_types),* + ) -> ::pyo3::PyResult<#return_annotation> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + #({ + let #positional_args_idents: ::pyo3::PyObject = #positional_args_idents.into_py(py); + #positional_args_idents + },)* + ); + let __internal_kwargs = ::pyo3::types::PyDict::new(py); + #(__internal_kwargs.set_item(::pyo3::intern!(py, #keyword_args_names), #keyword_args_idents)?;)* + py.import(::pyo3::intern!(py, #module_name))?.call_method(::pyo3::intern!(py, #function_name), __internal_args, Some(__internal_kwargs))?.extract() + } + } + }) +} diff --git a/pyo3_bindgen_engine/src/bindgen/module.rs b/pyo3_bindgen_engine/src/bindgen/module.rs new file mode 100644 index 0000000..f5f1c1c --- /dev/null +++ b/pyo3_bindgen_engine/src/bindgen/module.rs @@ -0,0 +1,274 @@ +use itertools::Itertools; + +use crate::bindgen::{bind_attribute, bind_class, bind_function}; + +/// Generate a Rust module from a Python module. This function is called recursively to generate +/// bindings for all submodules. The generated module will contain all classes, functions, and +/// attributes of the Python module. During the first call, the `root_module` argument should be +/// the same as the `module` argument and the `processed_modules` argument should be an empty +/// `HashSet`. +pub fn bind_module( + py: pyo3::Python, + root_module: &pyo3::types::PyModule, + module: &pyo3::types::PyModule, + processed_modules: &mut std::collections::HashSet, +) -> Result { + let inspect = py.import("inspect")?; + + // Extract the names of the modules + let root_module_name = root_module.name()?; + let full_module_name = module.name()?; + let module_name: &str = full_module_name.split('.').last().unwrap(); + + // Create the Rust module identifier (raw string if it is a keyword) + let module_ident = if syn::parse_str::(module_name).is_ok() { + quote::format_ident!("{module_name}") + } else { + quote::format_ident!("r#{module_name}") + }; + + // Iterate over all attributes of the module while updating the token stream + let mut mod_token_stream = proc_macro2::TokenStream::new(); + module + .dir() + .iter() + .map(|name| { + let name = name.str().unwrap().to_str().unwrap(); + let attr = module.getattr(name).unwrap(); + let attr_type = attr.get_type(); + (name, attr, attr_type) + }) + .filter(|&(_, _, attr_type)| { + // Skip builtin functions + !attr_type + .is_subclass_of::() + .unwrap_or(false) + }) + .filter(|&(name, _, _)| { + // Skip private attributes + !name.starts_with('_') || name == "__init__" || name == "__call__" + }) + .filter(|(_, attr, attr_type)| { + // Skip typing attributes + !attr + .getattr("__module__") + .is_ok_and(|module| module.to_string().contains("typing")) + && !attr_type.to_string().contains("typing") + }) + .filter(|(_, attr, _)| { + // Skip __future__ attributes + !attr + .getattr("__module__") + .is_ok_and(|module| module.to_string().contains("__future__")) + }) + .filter(|&(_, attr, _)| { + // Skip classes and functions that are not part of the package + // However, this should keep instances of classes and builtins even if they are builtins or from other packages + if let Ok(module) = attr.getattr("__module__") { + if module.to_string().starts_with(root_module_name) { + true + } else { + !(inspect + .call_method1("isclass", (attr,)) + .unwrap() + .is_true() + .unwrap() + || inspect + .call_method1("isfunction", (attr,)) + .unwrap() + .is_true() + .unwrap()) + } + } else { + true + } + }) + .filter(|&(_, attr, attr_type)| { + // Skip external modules + if attr_type + .is_subclass_of::() + .unwrap_or(false) + { + let is_submodule = attr + .getattr("__package__") + // Note: full_module_name is used here for comparison on purpose. + // It unseres that submodules are created in the correct scopes. + .is_ok_and(|package| package.to_string().starts_with(full_module_name)); + is_submodule + } else { + true + } + }) + .for_each(|(name, attr, attr_type)| { + let is_internal = attr + .getattr("__module__") + .unwrap_or(pyo3::types::PyString::new(py, "")) + .to_string() + .starts_with(root_module_name); + let is_reexport = is_internal + && attr + .getattr("__module__") + .unwrap_or(pyo3::types::PyString::new(py, "")) + .to_string() + .ne(full_module_name); + + let is_module = attr_type + .is_subclass_of::() + .unwrap_or(false); + + let is_class = attr_type + .is_subclass_of::() + .unwrap_or(false); + + let is_function = inspect + .call_method1("isfunction", (attr,)) + .unwrap() + .is_true() + .unwrap() + || inspect + .call_method1("ismethod", (attr,)) + .unwrap() + .is_true() + .unwrap(); + + // Make sure that only one of the three is true + debug_assert!(![is_module, is_class, is_function].iter().all(|&v| v)); + + // Process hidden modules (shadowed by re-exported attributes of the same name) + if (is_class || is_function) + && is_reexport + && attr + .getattr("__module__") + .unwrap() + .to_string() + .split('.') + .last() + .unwrap() + == name + && attr + .getattr("__module__") + .unwrap() + .to_string() + .split('.') + .take(full_module_name.split('.').count()) + .join(".") + == full_module_name + { + let content = if is_class { + bind_class(py, root_module, attr.downcast().unwrap()).unwrap() + } else if is_function { + bind_function(py, full_module_name, name, attr).unwrap() + } else { + unreachable!() + }; + + let shadowed_module_name = attr.getattr("__module__").unwrap().to_string(); + let shadowed_module_name = shadowed_module_name.split('.').last().unwrap(); + let shadowed_module_ident = + if syn::parse_str::(shadowed_module_name).is_ok() { + quote::format_ident!("{}", shadowed_module_name) + } else { + quote::format_ident!("r#{}", shadowed_module_name) + }; + + mod_token_stream.extend(quote::quote! { + pub mod #shadowed_module_ident { + #content + } + }); + } + + if is_module { + if processed_modules.insert(format!( + "{}.{}", + attr.getattr("__package__").unwrap(), + name + )) { + mod_token_stream.extend(bind_module( + py, + root_module, + attr.downcast().unwrap(), + processed_modules, + )); + } + } else if is_reexport { + mod_token_stream.extend(bind_reexport(full_module_name, name, attr)); + } else if is_class { + mod_token_stream.extend(bind_class(py, root_module, attr.downcast().unwrap())); + } else if is_function { + mod_token_stream.extend(bind_function(py, full_module_name, name, attr)); + } else { + mod_token_stream.extend(bind_attribute( + py, + Some(full_module_name), + name, + attr, + attr_type, + )); + } + }); + + let mut doc = module.getattr("__doc__")?.to_string(); + if doc == "None" { + doc = String::new(); + }; + + Ok(if module_name == root_module_name { + quote::quote! { + #mod_token_stream + } + } else { + quote::quote! { + #[doc = #doc] + pub mod #module_ident { + #mod_token_stream + } + } + }) +} + +/// Generate a re-export of an attribute from a submodule. This is commonly used in Python to +/// re-export attributes from submodules in the parent module. For example, `from os import path` +/// makes the `os.path` submodule available in the current module as just `path`. +pub fn bind_reexport( + module_name: &str, + name: &str, + attr: &pyo3::PyAny, +) -> Result { + let attr_origin_module = attr.getattr("__module__")?.to_string(); + let n_common_ancestors = module_name + .split('.') + .zip(attr_origin_module.split('.')) + .take_while(|(a, b)| a == b) + .count(); + let current_module_depth = module_name.split('.').count(); + let reexport_path: String = std::iter::repeat("super".to_string()) + .take(current_module_depth - n_common_ancestors) + .chain( + attr_origin_module + .split('.') + .skip(n_common_ancestors) + .map(|s| { + if syn::parse_str::(s).is_ok() { + s.to_owned() + } else { + format!("r#{s}") + } + }), + ) + .chain(std::iter::once(name).map(|s| { + if syn::parse_str::(s).is_ok() { + s.to_owned() + } else { + format!("r#{s}") + } + })) + .join("::"); + + // The path contains both ident and "::", combine into something that can be quoted + let reexport_path = syn::parse_str::(&reexport_path).unwrap(); + + Ok(quote::quote! { + pub use #reexport_path; + }) +} diff --git a/pyo3_bindgen_engine/src/build_utils.rs b/pyo3_bindgen_engine/src/build_utils.rs new file mode 100644 index 0000000..246c64b --- /dev/null +++ b/pyo3_bindgen_engine/src/build_utils.rs @@ -0,0 +1,56 @@ +//! Module with utilities for generating bindings in build scripts. + +/// Convenience function for generating bindings in build scripts. This function is equivalent to +/// calling `generate_bindings` and writing the result to a file. +/// +/// # Arguments +/// +/// * `module_name` - Name of the Python module to generate bindings for. +/// * `output_path` - Path to write the generated bindings to. +/// +/// # Returns +/// +/// `Result` containing `std::io::Error` on failure. +/// +/// # Example +/// +/// 1. Generate bindings using `build.rs` script. +/// +/// ```ignore +/// // build.rs +/// +/// // use pyo3_bindgen::build_bindings; +/// use pyo3_bindgen_engine::build_bindings; +/// +/// fn main() { +/// build_bindings( +/// "os", +/// std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("bindings.rs"), +/// ) +/// .unwrap(); +/// } +/// ``` +/// +/// 2. Include the generated bindings in `src/lib.rs`. +/// +/// ```ignore +/// // src/lib.rs +/// +/// #[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +/// pub mod os { +/// include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +/// } +/// ``` +// TODO: Add `println!("cargo:rerun-if-changed={}.py");` for all files of the target Python module +pub fn build_bindings( + module_name: &str, + output_path: impl AsRef, +) -> std::io::Result<()> { + let bindings = crate::generate_bindings(module_name).map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to generate bindings for Python module '{module_name}': {err}"), + ) + })?; + std::fs::write(output_path, bindings.to_string()) +} diff --git a/pyo3_bindgen_engine/src/lib.rs b/pyo3_bindgen_engine/src/lib.rs new file mode 100644 index 0000000..d4400ff --- /dev/null +++ b/pyo3_bindgen_engine/src/lib.rs @@ -0,0 +1,8 @@ +//! Engine for automatic generation of Rust FFI bindings to Python modules. + +pub mod bindgen; +pub mod build_utils; +pub mod types; + +pub use bindgen::{generate_bindings, generate_bindings_for_module, generate_bindings_from_str}; +pub use build_utils::build_bindings; diff --git a/pyo3_bindgen_engine/src/types.rs b/pyo3_bindgen_engine/src/types.rs new file mode 100644 index 0000000..3dcacc5 --- /dev/null +++ b/pyo3_bindgen_engine/src/types.rs @@ -0,0 +1,406 @@ +//! Module for handling Rust, Python and `PyO3` types. + +/// Map a Python type to a Rust type. +/// +/// Note that this is not a complete mapping at the moment. The public API is +/// subject to large changes. +/// +/// # Arguments +/// +/// * `attr_type` - The Python type to map (either a `PyType` or a `PyString`). +/// * `owned` - Whether the Rust type should be owned or not (e.g. `String` vs `&str`). +/// +/// # Returns +/// +/// The Rust type as a `TokenStream`. +// TODO: Support more complex type conversions +// TODO: Support module-wide classes/types for which bindings are generated +// TODO: Return `syn::Type` instead +// TODO: Refactor into something more elegant +pub fn map_attr_type( + attr_type: &pyo3::PyAny, + owned: bool, +) -> Result { + Ok( + if let Ok(attr_type) = attr_type.downcast::() { + match attr_type { + _string + if attr_type.is_subclass_of::()? + || attr_type.is_subclass_of::()? => + { + if owned { + quote::quote! { + ::std::string::String + } + } else { + quote::quote! { + &str + } + } + } + _bytes if attr_type.is_subclass_of::()? => { + if owned { + quote::quote! { + Vec + } + } else { + quote::quote! { + &[u8] + } + } + } + _bool if attr_type.is_subclass_of::()? => { + quote::quote! { + bool + } + } + _int if attr_type.is_subclass_of::()? => { + quote::quote! { + i64 + } + } + _float if attr_type.is_subclass_of::()? => { + quote::quote! { + f64 + } + } + // complex if attr_type.is_subclass_of::()? => { + // quote::quote! { + // todo!() + // } + // } + _list if attr_type.is_subclass_of::()? => { + quote::quote! { + Vec<&'py ::pyo3::types::PyAny> + } + } + _dict if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyDict + } + } + _tuple if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyTuple + } + } + _set if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PySet + } + } + _frozenset if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyFrozenSet + } + } + _bytearray if attr_type.is_subclass_of::()? => { + if owned { + quote::quote! { + Vec + } + } else { + quote::quote! { + &[u8] + } + } + } + _slice if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PySlice + } + } + _type if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyType + } + } + _module if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyModule + } + } + // collections_abc_Buffer + // if attr_type.is_subclass_of::>()? => + // { + // quote::quote! { + // &'py ::pyo3::types::PyBuffer + // } + // } + #[cfg(not(Py_LIMITED_API))] + _datetime_datetime if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyDateTime + } + } + #[cfg(not(Py_LIMITED_API))] + _datetime_date if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyDate + } + } + #[cfg(not(Py_LIMITED_API))] + _datetime_time if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyTime + } + } + #[cfg(not(Py_LIMITED_API))] + _datetime_tzinfo if attr_type.is_subclass_of::()? => { + quote::quote! { + &'py ::pyo3::types::PyTzInfo + } + } + #[cfg(not(Py_LIMITED_API))] + _timedelta if attr_type.is_subclass_of::()? => { + quote::quote! { + ::std::time::Duration + } + } + _unknown => { + quote::quote! { + &'py ::pyo3::types::PyAny + } + } + } + } else if let Ok(attr_type) = attr_type.downcast::() { + let attr_type = attr_type.to_str()?; + match attr_type { + "str" => { + if owned { + quote::quote! { + ::std::string::String + } + } else { + quote::quote! { + &str + } + } + } + "bytes" => { + if owned { + quote::quote! { + Vec + } + } else { + quote::quote! { + &[u8] + } + } + } + "bool" => { + quote::quote! { + bool + } + } + "int" => { + quote::quote! { + i64 + } + } + "float" => { + quote::quote! { + f64 + } + } + "complex" => { + quote::quote! { + &'py ::pyo3::types::PyComplex + } + } + list if list.starts_with("list[") && list.ends_with(']') => { + // TODO: Concrete type parsing for lists + quote::quote! { + Vec<&'py ::pyo3::types::PyAny> + } + } + dict if dict.starts_with("dict[") && dict.ends_with(']') => { + let (key, value) = dict + .strip_prefix("dict[") + .unwrap() + .strip_suffix(']') + .unwrap() + .split_once(',') + .unwrap(); + match (key, value) { + ("str", "Any") => { + quote::quote! { + ::std::collections::HashMap + } + } + ("str", "str") => { + quote::quote! { + ::std::collections::HashMap + } + } + ("str", "bool") => { + quote::quote! { + ::std::collections::HashMap + } + } + ("str", "int") => { + quote::quote! { + ::std::collections::HashMap + } + } + ("str", "float") => { + quote::quote! { + ::std::collections::HashMap + } + } + _unknown => { + quote::quote! { + &'py ::pyo3::types::PyDict + } + } + } + } + tuple if tuple.starts_with("tuple[") && tuple.ends_with(']') => { + // TODO: Concrete type parsing for tuple + quote::quote! { + &'py ::pyo3::types::PyTuple + } + } + set if set.starts_with("set[") && set.ends_with(']') => { + // TODO: Concrete type parsing for set + quote::quote! { + &'py ::pyo3::types::PySet + } + } + frozenset if frozenset.starts_with("frozenset[") && frozenset.ends_with(']') => { + // TODO: Concrete type parsing for frozenset + quote::quote! { + &'py ::pyo3::types::PyFrozenSet + } + } + "bytearray" => { + if owned { + quote::quote! { + Vec + } + } else { + quote::quote! { + &[u8] + } + } + } + "slice" => { + quote::quote! { + &'py ::pyo3::types::PySlice + } + } + "type" => { + quote::quote! { + &'py ::pyo3::types::PyType + } + } + "module" => { + quote::quote! { + &'py ::pyo3::types::PyModule + } + } + // "collections.abc.Buffer" => { + // quote::quote! { + // todo!() + // } + // } + "datetime.datetime" => { + quote::quote! { + &'py ::pyo3::types::PyDateTime + } + } + "datetime.date" => { + quote::quote! { + &'py ::pyo3::types::PyDate + } + } + "datetime.time" => { + quote::quote! { + &'py ::pyo3::types::PyTime + } + } + "datetime.tzinfo" => { + quote::quote! { + &'py ::pyo3::types::PyTzInfo + } + } + "timedelta" => { + quote::quote! { + ::std::time::Duration + } + } + // "decimal.Decimal" => { + // quote::quote! { + // todo!() + // } + // } + "ipaddress.IPv4Address" => { + quote::quote! { + ::std::net::IpV4Addr + } + } + "ipaddress.IPv6Address" => { + quote::quote! { + ::std::net::IpV6Addr + } + } + "os.PathLike" | "pathlib.Path" => { + if owned { + quote::quote! { + ::std::path::PathBuf + } + } else { + quote::quote! { + &::std::path::Path + } + } + } + optional if optional.ends_with(" | None") => { + let optional_type = optional.split_once('|').unwrap().0.trim_end(); + match optional_type { + "str" => { + if owned { + quote::quote! { + ::std::option::Option + } + } else { + quote::quote! { + ::std::option::Option<&str> + } + } + } + "bool" => { + quote::quote! { + ::std::option::Option + } + } + "int" => { + quote::quote! { + ::std::option::Option + } + } + "float" => { + quote::quote! { + ::std::option::Option + } + } + _unknown => { + quote::quote! { + ::std::option::Option<&'py ::pyo3::types::PyAny> + } + } + } + } + _unknown => { + quote::quote! { + &'py ::pyo3::types::PyAny + } + } + } + } else { + quote::quote! { + &'py ::pyo3::types::PyAny + } + }, + ) +} diff --git a/pyo3_bindgen_engine/tests/bindgen.rs b/pyo3_bindgen_engine/tests/bindgen.rs new file mode 100644 index 0000000..3a1ab26 --- /dev/null +++ b/pyo3_bindgen_engine/tests/bindgen.rs @@ -0,0 +1,261 @@ +macro_rules! test_bindgen { + { + $(#[$meta:meta])* + $test_name:ident $(,)? + $(py)?$(python)? $(:)? $code_py:literal $(,)? + $(rs)?$(rust)? $(:)? $code_rs:literal $(,)? + } => { + #[test] + $(#[$meta])* + fn $test_name() { + // Arrange + const CODE_PY: &str = indoc::indoc! { $code_py }; + const CODE_RS: &str = indoc::indoc! { $code_rs }; + + // Act + let bindings = pyo3_bindgen_engine::generate_bindings_from_str( + CODE_PY, + concat!("t_mod_", stringify!($test_name)), + ) + .unwrap(); + + // Assert + let generated_code = format_code(&bindings.to_string()); + let target_code = format_code(CODE_RS); + assert_eq!( + generated_code, target_code, + "\nGenerated:\n\n{generated_code}" + ); + } + }; +} + +fn format_code(input: &str) -> String { + prettyplease::unparse(&syn::parse_str(input).unwrap()) +} + +test_bindgen! { + test_bindgen_attribute + + py:r#" + t_const_float: float = 0.42 + "# + + rs:r#" + ///Getter for the `t_const_float` attribute + pub fn t_const_float<'py>(py: ::pyo3::marker::Python<'py>) -> ::pyo3::PyResult { + py.import(::pyo3::intern!(py, "t_mod_test_bindgen_attribute"))? + .getattr(::pyo3::intern!(py, "t_const_float"))? + .extract() + } + ///Setter for the `t_const_float` attribute + pub fn set_t_const_float<'py>( + py: ::pyo3::marker::Python<'py>, + value: f64, + ) -> ::pyo3::PyResult<()> { + py.import(::pyo3::intern!(py, "t_mod_test_bindgen_attribute"))? + .setattr(::pyo3::intern!(py, "t_const_float"), value)?; + Ok(()) + } + "# +} + +test_bindgen! { + test_bindgen_function + + py:r#" + def t_fn(t_arg1: str) -> int: + """t_docs""" + ... + "# + + rs:r#" + ///t_docs + pub fn t_fn<'py>( + py: ::pyo3::marker::Python<'py>, + t_arg1: &str, + ) -> ::pyo3::PyResult { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + { + let t_arg1: ::pyo3::PyObject = t_arg1.into_py(py); + t_arg1 + }, + ); + let __internal_kwargs = ::pyo3::types::PyDict::new(py); + py.import(::pyo3::intern!(py, "t_mod_test_bindgen_function"))? + .call_method( + ::pyo3::intern!(py, "t_fn"), + __internal_args, + Some(__internal_kwargs), + )? + .extract() + } + "# +} + +test_bindgen! { + test_bindgen_class + + py:r#" + from typing import Dict, Optional + class t_class: + """t_docs""" + def __init__(self, t_arg1: str, t_arg2: Optional[int] = None): + """t_docs_init""" + ... + def t_method(self, t_arg1: Dict[str, int], **kwargs): + """t_docs_method""" + ... + @property + def t_prop(self) -> int: + ... + @t_prop.setter + def t_prop(self, value: int): + ... + "# + + rs:r#" + ///t_docs + #[repr(transparent)] + #[derive(Clone, Debug)] + pub struct t_class(pub ::pyo3::PyObject); + #[automatically_derived] + impl ::std::ops::Deref for t_class { + type Target = ::pyo3::PyObject; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + #[automatically_derived] + impl ::std::ops::DerefMut for t_class { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + #[automatically_derived] + impl<'py> ::pyo3::FromPyObject<'py> for t_class { + fn extract(value: &'py ::pyo3::PyAny) -> ::pyo3::PyResult { + Ok(Self(value.into())) + } + } + #[automatically_derived] + impl ::pyo3::ToPyObject for t_class { + fn to_object<'py>(&'py self, py: ::pyo3::Python<'py>) -> ::pyo3::PyObject { + self.as_ref(py).to_object(py) + } + } + #[automatically_derived] + impl From<::pyo3::PyObject> for t_class { + fn from(value: ::pyo3::PyObject) -> Self { + Self(value) + } + } + #[automatically_derived] + impl<'py> From<&'py ::pyo3::PyAny> for t_class { + fn from(value: &'py ::pyo3::PyAny) -> Self { + Self(value.into()) + } + } + #[automatically_derived] + impl t_class { + ///t_docs_init + pub fn __init__<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + t_arg1: &str, + t_arg2: &'py ::pyo3::types::PyAny, + ) -> ::pyo3::PyResult<&'py ::pyo3::types::PyAny> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + { + let t_arg1: ::pyo3::PyObject = t_arg1.into_py(py); + t_arg1 + }, + { + let t_arg2: ::pyo3::PyObject = t_arg2.into_py(py); + t_arg2 + }, + ); + let __internal_kwargs = ::pyo3::types::PyDict::new(py); + self.as_ref(py) + .call_method( + ::pyo3::intern!(py, "__init__"), + __internal_args, + Some(__internal_kwargs), + )? + .extract() + } + ///t_docs_method + pub fn t_method<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + t_arg1: &'py ::pyo3::types::PyAny, + kwargs: &'py ::pyo3::types::PyDict, + ) -> ::pyo3::PyResult<&'py ::pyo3::types::PyAny> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + { + let t_arg1: ::pyo3::PyObject = t_arg1.into_py(py); + t_arg1 + }, + ); + let __internal_kwargs = kwargs; + self.as_ref(py) + .call_method( + ::pyo3::intern!(py, "t_method"), + __internal_args, + Some(__internal_kwargs), + )? + .extract() + } + ///Getter for the `t_prop` attribute + pub fn t_prop<'py>( + &'py self, + py: ::pyo3::marker::Python<'py>, + ) -> ::pyo3::PyResult { + self.as_ref(py).getattr(::pyo3::intern!(py, "t_prop"))?.extract() + } + ///Setter for the `t_prop` attribute + pub fn set_t_prop<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + value: i64, + ) -> ::pyo3::PyResult<()> { + self.as_ref(py).setattr(::pyo3::intern!(py, "t_prop"), value)?; + Ok(()) + } + ///t_docs_init + pub fn new<'py>( + &'py mut self, + py: ::pyo3::marker::Python<'py>, + t_arg1: &str, + t_arg2: &'py ::pyo3::types::PyAny, + ) -> ::pyo3::PyResult<&'py ::pyo3::types::PyAny> { + #[allow(unused_imports)] + use ::pyo3::IntoPy; + let __internal_args = ( + { + let t_arg1: ::pyo3::PyObject = t_arg1.into_py(py); + t_arg1 + }, + { + let t_arg2: ::pyo3::PyObject = t_arg2.into_py(py); + t_arg2 + }, + ); + let __internal_kwargs = ::pyo3::types::PyDict::new(py); + self.as_ref(py) + .call_method( + ::pyo3::intern!(py, "__init__"), + __internal_args, + Some(__internal_kwargs), + )? + .extract() + } + } + "# +} diff --git a/pyo3_bindgen_macros/Cargo.toml b/pyo3_bindgen_macros/Cargo.toml new file mode 100644 index 0000000..36da026 --- /dev/null +++ b/pyo3_bindgen_macros/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pyo3_bindgen_macros" +authors.workspace = true +categories.workspace = true +description = "Procedural macros for automatic generation of Rust bindings to Python modules" +edition.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +proc-macro2 = { workspace = true } +pyo3_bindgen_engine = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +pyo3 = { workspace = true } + +[lib] +proc-macro = true diff --git a/pyo3_bindgen_macros/src/lib.rs b/pyo3_bindgen_macros/src/lib.rs new file mode 100644 index 0000000..280e5f2 --- /dev/null +++ b/pyo3_bindgen_macros/src/lib.rs @@ -0,0 +1,35 @@ +//! Procedural macros for automatic generation of Rust FFI bindings to Python modules. + +mod parser; + +/// Procedural macro for generating Rust bindings to Python modules in-place. +/// +/// # Panics +/// +/// Panics if the bindings cannot be generated. +/// +/// # Example +/// +/// ```ignore +/// // use pyo3_bindgen::import_python; +/// use pyo3_bindgen_macros::import_python; +/// +/// #[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +/// pub mod sys { +/// import_python!("sys"); +/// } +/// +/// #[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +/// pub(crate) mod os_path { +/// import_python!("os.path"); +/// } +/// ``` +#[proc_macro] +pub fn import_python(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let parser::Args { module_name_py } = syn::parse_macro_input!(input as parser::Args); + + // Generate the bindings + pyo3_bindgen_engine::generate_bindings(&module_name_py) + .unwrap_or_else(|_| panic!("Failed to generate bindings for module: {module_name_py}")) + .into() +} diff --git a/pyo3_bindgen_macros/src/parser.rs b/pyo3_bindgen_macros/src/parser.rs new file mode 100644 index 0000000..1110419 --- /dev/null +++ b/pyo3_bindgen_macros/src/parser.rs @@ -0,0 +1,20 @@ +//! Parsing of procedural macro arguments. + +use syn::{ + parse::{Parse, ParseStream, Result}, + LitStr, +}; + +/// Arguments for the `import_python` procedural macro +pub struct Args { + /// Name of the Python module to generate bindings for + pub module_name_py: String, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> Result { + // Python module name might contain dots, so it is parsed as a string literal + let module_name_py = input.parse::()?.value(); + Ok(Args { module_name_py }) + } +}