├── .gitattributes ├── .github ├── actions │ └── setup-test-environment │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── python.yml │ ├── release.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── Taskfile.yml ├── examples ├── mixed │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Taskfile.yml │ ├── pyproject.toml │ ├── python │ │ └── mixed │ │ │ ├── __init__.py │ │ │ ├── main_mod.pyi │ │ │ └── py.typed │ ├── src │ │ ├── bin │ │ │ └── stub_gen.rs │ │ └── lib.rs │ └── tests │ │ └── test_mixed.py ├── mixed_sub │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Taskfile.yml │ ├── pyproject.toml │ ├── python │ │ └── mixed_sub │ │ │ ├── __init__.py │ │ │ ├── main_mod │ │ │ ├── __init__.pyi │ │ │ ├── int.pyi │ │ │ └── sub_mod.pyi │ │ │ └── py.typed │ ├── src │ │ ├── bin │ │ │ └── stub_gen.rs │ │ └── lib.rs │ └── tests │ │ └── test_mixed_sub.py ├── mixed_sub_multiple │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Taskfile.yml │ ├── pyproject.toml │ ├── python │ │ └── mixed_sub_multiple │ │ │ ├── main_mod │ │ │ ├── __init__.pyi │ │ │ ├── mod_a.pyi │ │ │ └── mod_b.pyi │ │ │ └── py.typed │ ├── src │ │ ├── bin │ │ │ └── stub_gen.rs │ │ └── lib.rs │ └── tests │ │ └── test_mixed_sub_multiple.py ├── pure │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Taskfile.yml │ ├── pure.pyi │ ├── pyproject.toml │ ├── src │ │ ├── bin │ │ │ └── stub_gen.rs │ │ └── lib.rs │ └── tests │ │ └── test_python.py └── pure_abi3 │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Taskfile.yml │ ├── pure_abi3.pyi │ ├── pyproject.toml │ ├── src │ ├── bin │ │ └── stub_gen.rs │ └── lib.rs │ └── tests │ └── test_python.py ├── pyo3-stub-gen-derive ├── Cargo.toml └── src │ ├── gen_stub.rs │ ├── gen_stub │ ├── arg.rs │ ├── attr.rs │ ├── member.rs │ ├── method.rs │ ├── pyclass.rs │ ├── pyclass_enum.rs │ ├── pyfunction.rs │ ├── pymethods.rs │ ├── renaming.rs │ ├── signature.rs │ ├── stub_type.rs │ └── util.rs │ └── lib.rs ├── pyo3-stub-gen ├── Cargo.toml ├── build.rs └── src │ ├── exception.rs │ ├── generate.rs │ ├── generate │ ├── arg.rs │ ├── class.rs │ ├── docstring.rs │ ├── enum_.rs │ ├── error.rs │ ├── function.rs │ ├── member.rs │ ├── method.rs │ ├── module.rs │ ├── stub_info.rs │ └── variable.rs │ ├── lib.rs │ ├── pyproject.rs │ ├── stub_type.rs │ ├── stub_type │ ├── builtins.rs │ ├── collections.rs │ ├── either.rs │ ├── numpy.rs │ └── pyo3.rs │ ├── type_info.rs │ └── util.rs ├── pyproject.toml └── uv.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock linguist-generated=true 2 | uv.lock linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/actions/setup-test-environment/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Test Environment" 2 | description: "Setup common environment for testing jobs" 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Install Task 8 | uses: arduino/setup-task@v2 9 | with: 10 | version: 3.x 11 | repo-token: ${{ github.token }} 12 | 13 | - name: Setup Rust 14 | uses: dtolnay/rust-toolchain@stable 15 | with: 16 | components: rustfmt, clippy 17 | 18 | - name: Setup caching for Rust 19 | uses: Swatinem/rust-cache@v2 20 | 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@v3 23 | with: 24 | python-version: 3.9 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | open-pull-requests-limit: 10 9 | allow: 10 | # Allow both direct and indirect updates for all packages 11 | - dependency-type: "all" 12 | groups: 13 | dependencies: 14 | patterns: 15 | - "*" 16 | 17 | - package-ecosystem: "cargo" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | open-pull-requests-limit: 10 22 | allow: 23 | # Allow both direct and indirect updates for all packages 24 | - dependency-type: "all" 25 | # Ignore specific dependencies 26 | ignore: 27 | - dependency-name: "pyo3" 28 | - dependency-name: "numpy" 29 | groups: 30 | dependencies: 31 | patterns: 32 | - "*" 33 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | # Allow one concurrent deployment 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | rust: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Rust 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: stable 32 | 33 | - name: Generate rustdoc 34 | run: cargo doc --all --no-deps --document-private-items 35 | 36 | - name: Upload html 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: rustdoc 40 | path: ./target/doc 41 | retention-days: 30 42 | 43 | # See https://github.com/actions/deploy-pages#usage 44 | deploy: 45 | needs: [rust] 46 | environment: 47 | name: "Document" 48 | url: ${{ steps.deployment.outputs.page_url }}pyo3_stub_gen/index.html 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Download rustdoc 52 | uses: actions/download-artifact@v4 53 | with: 54 | name: rustdoc 55 | path: . 56 | 57 | - name: Configure GitHub Pages 58 | uses: actions/configure-pages@v5 59 | 60 | - name: Upload artifact 61 | uses: actions/upload-pages-artifact@v3 62 | with: 63 | # Upload entire repository 64 | path: "." 65 | 66 | - name: Deploy to GitHub Pages 67 | id: deployment 68 | uses: actions/deploy-pages@v4 69 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | crate: 17 | - pure_abi3 18 | - pure 19 | - mixed 20 | - mixed_sub 21 | - mixed_sub_multiple 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup Environment 26 | uses: ./.github/actions/setup-test-environment 27 | - name: Test 28 | run: task ${{ matrix.crate }}:test 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: cargo publish 17 | run: | 18 | cargo publish -p pyo3-stub-gen-derive 19 | cargo publish -p pyo3-stub-gen 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 22 | 23 | next_version: 24 | runs-on: ubuntu-latest 25 | needs: publish 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Environment 31 | uses: ./.github/actions/setup-test-environment 32 | 33 | - name: Install cargo-edit 34 | run: cargo install cargo-edit 35 | 36 | - name: Bump version 37 | run: | 38 | cargo set-version --bump patch 39 | echo "NEW_VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version')" >> $GITHUB_ENV 40 | 41 | - name: Update Cargo.lock 42 | run: task generate-lockfile 43 | 44 | - name: Create Pull Request 45 | uses: peter-evans/create-pull-request@v7 46 | with: 47 | title: "Start developing ${{ env.NEW_VERSION }}" 48 | branch: "rust-version-update/${{ env.NEW_VERSION }}" 49 | base: "main" 50 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | doc-check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Setup Environment 17 | uses: ./.github/actions/setup-test-environment 18 | - name: Check warnings in documents 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: doc 22 | args: --no-deps 23 | env: 24 | RUSTDOCFLAGS: -D warnings 25 | 26 | fmt: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Setup Environment 32 | uses: ./.github/actions/setup-test-environment 33 | - name: Run cargo fmt 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: fmt 37 | args: --all -- --check 38 | 39 | clippy: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | - name: Setup Environment 45 | uses: ./.github/actions/setup-test-environment 46 | - name: Check with clippy 47 | uses: actions-rs/clippy-check@v1 48 | with: 49 | args: --all-features -- -D warnings 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | test: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | - name: Setup Environment 58 | uses: ./.github/actions/setup-test-environment 59 | - name: Run tests 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: test 63 | 64 | stub-gen: 65 | runs-on: ubuntu-latest 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | crate: 70 | - pure 71 | - pure_abi3 72 | - mixed 73 | - mixed_sub 74 | - mixed_sub_multiple 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | - name: Setup Environment 79 | uses: ./.github/actions/setup-test-environment 80 | - name: Generate stub file 81 | run: task ${{ matrix.crate}}:stub-gen 82 | - name: Check if stub file is up to date 83 | run: git diff --exit-code 84 | 85 | semver-check: 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | - name: Check semver 91 | uses: obi1kenobi/cargo-semver-checks-action@v2 92 | with: 93 | package: pyo3-stub-gen, pyo3-stub-gen-derive 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # MSVC Windows builds of rustc generate these, which store debugging information 9 | *.pdb 10 | 11 | # PyO3 development 12 | .venv*/ 13 | __pycache__/ 14 | *.so 15 | *.pyc 16 | dist/ 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["pyo3-stub-gen", "pyo3-stub-gen-derive"] 3 | exclude = [ 4 | # Note: "pure_abi3" and "pure" cannot be built simultaneously 5 | # since the ABI3 version of PyO3 is not compatible with the native version 6 | "examples/pure_abi3", 7 | "examples/pure", 8 | "examples/mixed", 9 | "examples/mixed_sub", 10 | "examples/mixed_sub_multiple", 11 | ] 12 | resolver = "2" 13 | 14 | [workspace.package] 15 | version = "0.9.1" 16 | edition = "2021" 17 | 18 | description = "Stub file (*.pyi) generator for PyO3" 19 | repository = "https://github.com/Jij-Inc/pyo3-stub-gen" 20 | keywords = ["pyo3"] 21 | license = "MIT OR Apache-2.0" 22 | readme = "README.md" 23 | 24 | [workspace.dependencies] 25 | either = "1.15.0" 26 | ahash = "0.8.11" 27 | anyhow = "1.0.98" 28 | chrono = "0.4.40" 29 | env_logger = "0.11.8" 30 | heck = "0.5" 31 | indexmap = ">= 2.7.0" 32 | insta = "1.43.0" 33 | inventory = "0.3.20" 34 | itertools = "0.13.0" 35 | log = "0.4.27" 36 | maplit = "1.0.2" 37 | num-complex = "0.4.6" 38 | numpy = ">= 0.24.0" 39 | prettyplease = "0.2.32" 40 | proc-macro2 = "1.0.95" 41 | pyo3 = ">= 0.24.0" 42 | quote = "1.0.40" 43 | serde = { version = "1.0.219", features = ["derive"] } 44 | syn = "2.0.101" 45 | toml = "0.8.21" 46 | test-case = "3.3.1" 47 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jij Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyo3-stub-gen 2 | 3 | [![DeepWiki](https://img.shields.io/badge/DeepWiki-Jij--Inc%2Fpyo3--stub--gen-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/Jij-Inc/pyo3-stub-gen) 4 | 5 | Python stub file (`*.pyi`) generator for [PyO3] with [maturin] projects. 6 | 7 | [PyO3]: https://github.com/PyO3/pyo3 8 | [maturin]: https://github.com/PyO3/maturin 9 | 10 | | crate name | crates.io | docs.rs | doc (main) | 11 | | --- | --- | --- | --- | 12 | | [pyo3-stub-gen] | [![crate](https://img.shields.io/crates/v/pyo3-stub-gen.svg)](https://crates.io/crates/pyo3-stub-gen) | [![docs.rs](https://docs.rs/pyo3-stub-gen/badge.svg)](https://docs.rs/pyo3-stub-gen) | [![doc (main)](https://img.shields.io/badge/doc-main-blue?logo=github)](https://jij-inc.github.io/pyo3-stub-gen/pyo3_stub_gen/index.html) | 13 | | [pyo3-stub-gen-derive] | [![crate](https://img.shields.io/crates/v/pyo3-stub-gen-derive.svg)](https://crates.io/crates/pyo3-stub-gen-derive) | [![docs.rs](https://docs.rs/pyo3-stub-gen-derive/badge.svg)](https://docs.rs/pyo3-stub-gen-derive) | [![doc (main)](https://img.shields.io/badge/doc-main-blue?logo=github)](https://jij-inc.github.io/pyo3-stub-gen/pyo3_stub_gen_derive/index.html) | 14 | 15 | [pyo3-stub-gen]: ./pyo3-stub-gen/ 16 | [pyo3-stub-gen-derive]: ./pyo3-stub-gen-derive/ 17 | 18 | # Design 19 | Our goal is to create a stub file `*.pyi` from Rust code, however, 20 | automated complete translation is impossible due to the difference between Rust and Python type systems and the limitation of proc-macro. 21 | We take semi-automated approach: 22 | 23 | - Provide a default translator which will work **most** cases, not **all** cases 24 | - Also provide a manual way to specify the translation. 25 | 26 | If the default translator does not work, users can specify the translation manually, 27 | and these manual translations can be integrated with what the default translator generates. 28 | So the users can use the default translator as much as possible and only specify the translation for the edge cases. 29 | 30 | [pyo3-stub-gen] crate provides the manual way to specify the translation, 31 | and [pyo3-stub-gen-derive] crate provides the default translator as proc-macro based on the mechanism of [pyo3-stub-gen]. 32 | 33 | # Usage 34 | 35 | If you are looking for a working example, please see the [examples](./examples/) directory. 36 | 37 | | Example | Description | 38 | |:-----------------|:------------| 39 | | [examples/pure] | Example for [Pure Rust maturin project](https://www.maturin.rs/project_layout#pure-rust-project) | 40 | | [examples/mixed] | Example for [Mixed Rust/Python maturin project](https://www.maturin.rs/project_layout#mixed-rustpython-project) | 41 | | [examples/mixed_sub] | Example for [Mixed Rust/Python maturin project](https://www.maturin.rs/project_layout#mixed-rustpython-project) with submodule | 42 | 43 | [examples/pure]: ./examples/pure/ 44 | [examples/mixed]: ./examples/mixed/ 45 | [examples/mixed_sub]: ./examples/mixed_sub/ 46 | 47 | Here we describe basic usage of [pyo3-stub-gen] crate based on [examples/pure] example. 48 | 49 | ## Annotate Rust code with proc-macro 50 | 51 | This crate provides a procedural macro `#[gen_stub_pyfunction]` and others to generate a Python stub file. 52 | It is used with PyO3's `#[pyfunction]` macro. Let's consider a simple example PyO3 project: 53 | 54 | ```rust 55 | use pyo3::prelude::*; 56 | 57 | #[pyfunction] 58 | fn sum_as_string(a: usize, b: usize) -> PyResult { 59 | Ok((a + b).to_string()) 60 | } 61 | 62 | #[pymodule] 63 | fn your_module_name(m: &Bound) -> PyResult<()> { 64 | m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; 65 | Ok(()) 66 | } 67 | ``` 68 | 69 | To generate a stub file for this project, please modify it as follows: 70 | 71 | ```rust 72 | use pyo3::prelude::*; 73 | use pyo3_stub_gen::{derive::gen_stub_pyfunction, define_stub_info_gatherer}; 74 | 75 | #[gen_stub_pyfunction] // Proc-macro attribute to register a function to stub file generator. 76 | #[pyfunction] 77 | fn sum_as_string(a: usize, b: usize) -> PyResult { 78 | Ok((a + b).to_string()) 79 | } 80 | 81 | #[pymodule] 82 | fn your_module_name(m: &Bound) -> PyResult<()> { 83 | m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; 84 | Ok(()) 85 | } 86 | 87 | // Define a function to gather stub information. 88 | define_stub_info_gatherer!(stub_info); 89 | ``` 90 | 91 | > [!NOTE] 92 | > The `#[gen_stub_pyfunction]` macro must be placed before `#[pyfunction]` macro. 93 | 94 | ## Generate a stub file 95 | 96 | And then, create an executable target in [`src/bin/stub_gen.rs`](./examples/pure/src/bin/stub_gen.rs) to generate a stub file: 97 | 98 | ```rust 99 | use pyo3_stub_gen::Result; 100 | 101 | fn main() -> Result<()> { 102 | // `stub_info` is a function defined by `define_stub_info_gatherer!` macro. 103 | let stub = pure::stub_info()?; 104 | stub.generate()?; 105 | Ok(()) 106 | } 107 | ``` 108 | 109 | and add `rlib` in addition to `cdylib` in `[lib]` section of `Cargo.toml`: 110 | 111 | ```toml 112 | [lib] 113 | crate-type = ["cdylib", "rlib"] 114 | ``` 115 | 116 | This target generates a stub file [`pure.pyi`](./examples/pure/pure.pyi) when executed. 117 | 118 | ```shell 119 | cargo run --bin stub_gen 120 | ``` 121 | 122 | The stub file is automatically found by `maturin`, and it is included in the wheel package. See also the [maturin document](https://www.maturin.rs/project_layout#adding-python-type-information) for more details. 123 | 124 | # Contribution 125 | To be written. 126 | 127 | # License 128 | 129 | © 2024 Jij Inc. 130 | 131 | This project is licensed under either of 132 | 133 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 134 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 135 | 136 | at your option. 137 | 138 | # Links 139 | 140 | - [MusicalNinjas/pyo3-stubgen](https://github.com/MusicalNinjas/pyo3-stubgen) 141 | - Same motivation, but different approach. 142 | - This project creates a stub file by loading the compiled library and inspecting the `__text_signature__` attribute generated by PyO3 in Python side. 143 | - [pybind11-stubgen](https://github.com/sizmailov/pybind11-stubgen) 144 | - Stub file generator for [pybind11](https://github.com/pybind/pybind11) based C++ projects. 145 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | # yaml-language-server: $schema=https://taskfile.dev/schema.json 3 | version: "3" 4 | 5 | includes: 6 | pure_abi3: 7 | taskfile: examples/pure_abi3/Taskfile.yml 8 | dir: examples/pure_abi3 9 | pure: 10 | taskfile: examples/pure/Taskfile.yml 11 | dir: examples/pure 12 | mixed: 13 | taskfile: examples/mixed/Taskfile.yml 14 | dir: examples/mixed 15 | mixed_sub: 16 | taskfile: examples/mixed_sub/Taskfile.yml 17 | dir: examples/mixed_sub 18 | mixed_sub_multiple: 19 | taskfile: examples/mixed_sub_multiple/Taskfile.yml 20 | dir: examples/mixed_sub_multiple 21 | 22 | tasks: 23 | stub-gen: 24 | cmds: 25 | - task: pure_abi3:stub-gen 26 | - task: pure:stub-gen 27 | - task: mixed:stub-gen 28 | - task: mixed_sub:stub-gen 29 | - task: mixed_sub_multiple:stub-gen 30 | 31 | generate-lockfile: 32 | cmds: 33 | - task: pure_abi3:generate-lockfile 34 | - task: pure:generate-lockfile 35 | - task: mixed:generate-lockfile 36 | - task: mixed_sub:generate-lockfile 37 | - task: mixed_sub_multiple:generate-lockfile 38 | 39 | test: 40 | cmds: 41 | - task: pure_abi3:test 42 | - task: pure:test 43 | - task: mixed:test 44 | - task: mixed_sub:test 45 | - task: mixed_sub_multiple:test 46 | -------------------------------------------------------------------------------- /examples/mixed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mixed" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Example for Mixed Rust/Python layout" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | env_logger = "0.11.8" 12 | pyo3 = ">= 0.24.0" 13 | pyo3-stub-gen = { path = "../../pyo3-stub-gen" } 14 | 15 | [[bin]] 16 | name = "stub_gen" 17 | doc = false 18 | -------------------------------------------------------------------------------- /examples/mixed/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | # yaml-language-server: $schema=https://taskfile.dev/schema.json 3 | version: "3" 4 | 5 | tasks: 6 | stub-gen: 7 | desc: Generate stub file 8 | cmds: 9 | - cargo run --bin stub_gen 10 | 11 | generate-lockfile: 12 | desc: Generate lockfile 13 | cmds: 14 | - cargo generate-lockfile 15 | 16 | test: 17 | desc: Run tests 18 | cmds: 19 | - uv run pytest 20 | - uv run pyright 21 | - uvx ruff check 22 | -------------------------------------------------------------------------------- /examples/mixed/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.1,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "mixed" 7 | version = "0.1" 8 | requires-python = ">=3.9" 9 | 10 | [tool.maturin] 11 | python-source = "python" 12 | module-name = "mixed.main_mod" 13 | features = ["pyo3/extension-module"] 14 | -------------------------------------------------------------------------------- /examples/mixed/python/mixed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jij-Inc/pyo3-stub-gen/5e772fe386df68fb764b98cf8eb7771194a630a7/examples/mixed/python/mixed/__init__.py -------------------------------------------------------------------------------- /examples/mixed/python/mixed/main_mod.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | import builtins 5 | 6 | class A: 7 | def show_x(self) -> None: ... 8 | 9 | class B: 10 | def show_x(self) -> None: ... 11 | 12 | def create_a(x:builtins.int) -> A: ... 13 | 14 | def create_b(x:builtins.int) -> B: ... 15 | 16 | -------------------------------------------------------------------------------- /examples/mixed/python/mixed/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jij-Inc/pyo3-stub-gen/5e772fe386df68fb764b98cf8eb7771194a630a7/examples/mixed/python/mixed/py.typed -------------------------------------------------------------------------------- /examples/mixed/src/bin/stub_gen.rs: -------------------------------------------------------------------------------- 1 | use pyo3_stub_gen::Result; 2 | 3 | fn main() -> Result<()> { 4 | env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init(); 5 | let stub = mixed::stub_info()?; 6 | stub.generate()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /examples/mixed/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3_stub_gen::{define_stub_info_gatherer, derive::*}; 3 | 4 | // Specify the module name explicitly 5 | #[gen_stub_pyclass] 6 | #[pyclass(module = "mixed.main_mod")] 7 | #[derive(Debug)] 8 | struct A { 9 | x: usize, 10 | } 11 | 12 | #[gen_stub_pymethods] 13 | #[pymethods] 14 | impl A { 15 | fn show_x(&self) { 16 | println!("x = {}", self.x); 17 | } 18 | } 19 | 20 | #[gen_stub_pyfunction(module = "mixed.main_mod")] 21 | #[pyfunction] 22 | fn create_a(x: usize) -> A { 23 | A { x } 24 | } 25 | 26 | // Do not specify the module name explicitly 27 | // This will be placed in the main module 28 | #[gen_stub_pyclass] 29 | #[pyclass] 30 | #[derive(Debug)] 31 | struct B { 32 | x: usize, 33 | } 34 | 35 | #[gen_stub_pymethods] 36 | #[pymethods] 37 | impl B { 38 | fn show_x(&self) { 39 | println!("x = {}", self.x); 40 | } 41 | } 42 | 43 | #[gen_stub_pyfunction] 44 | #[pyfunction] 45 | fn create_b(x: usize) -> B { 46 | B { x } 47 | } 48 | 49 | #[pymodule] 50 | fn main_mod(m: &Bound) -> PyResult<()> { 51 | m.add_class::()?; 52 | m.add_class::()?; 53 | m.add_function(wrap_pyfunction!(create_a, m)?)?; 54 | m.add_function(wrap_pyfunction!(create_b, m)?)?; 55 | Ok(()) 56 | } 57 | 58 | define_stub_info_gatherer!(stub_info); 59 | 60 | /// Test of unit test for testing link problem 61 | #[cfg(test)] 62 | mod test { 63 | #[test] 64 | fn test() { 65 | assert_eq!(2 + 2, 4); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/mixed/tests/test_mixed.py: -------------------------------------------------------------------------------- 1 | from mixed import main_mod 2 | 3 | 4 | def test_main_mod(): 5 | a = main_mod.create_a(1) 6 | a.show_x() 7 | 8 | b = main_mod.create_b(1) 9 | b.show_x() 10 | -------------------------------------------------------------------------------- /examples/mixed_sub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mixed_sub" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Example for Mixed Rust/Python layout with submodule" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | env_logger = "0.11.8" 12 | pyo3 = ">= 0.24.0" 13 | pyo3-stub-gen = { path = "../../pyo3-stub-gen" } 14 | 15 | [[bin]] 16 | name = "stub_gen" 17 | doc = false 18 | -------------------------------------------------------------------------------- /examples/mixed_sub/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | # yaml-language-server: $schema=https://taskfile.dev/schema.json 3 | version: "3" 4 | 5 | tasks: 6 | stub-gen: 7 | desc: Generate stub file 8 | cmds: 9 | - cargo run --bin stub_gen 10 | 11 | generate-lockfile: 12 | desc: Generate lockfile 13 | cmds: 14 | - cargo generate-lockfile 15 | 16 | test: 17 | desc: Run tests 18 | cmds: 19 | - uv run pytest 20 | - uv run pyright 21 | - uvx ruff check 22 | -------------------------------------------------------------------------------- /examples/mixed_sub/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.1,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "mixed_sub" 7 | version = "0.1" 8 | requires-python = ">=3.9" 9 | 10 | [tool.maturin] 11 | python-source = "python" 12 | module-name = "mixed_sub.main_mod" 13 | features = ["pyo3/extension-module"] 14 | -------------------------------------------------------------------------------- /examples/mixed_sub/python/mixed_sub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jij-Inc/pyo3-stub-gen/5e772fe386df68fb764b98cf8eb7771194a630a7/examples/mixed_sub/python/mixed_sub/__init__.py -------------------------------------------------------------------------------- /examples/mixed_sub/python/mixed_sub/main_mod/__init__.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | import builtins 5 | from . import int 6 | from . import sub_mod 7 | 8 | class A: 9 | def show_x(self) -> None: ... 10 | 11 | class B: 12 | def show_x(self) -> None: ... 13 | 14 | def create_a(x:builtins.int) -> A: ... 15 | 16 | def create_b(x:builtins.int) -> B: ... 17 | 18 | -------------------------------------------------------------------------------- /examples/mixed_sub/python/mixed_sub/main_mod/int.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | import builtins 5 | 6 | def dummy_int_fun(x:builtins.int) -> builtins.int: ... 7 | 8 | -------------------------------------------------------------------------------- /examples/mixed_sub/python/mixed_sub/main_mod/sub_mod.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | import builtins 5 | 6 | class C: 7 | def show_x(self) -> None: ... 8 | 9 | def create_c(x:builtins.int) -> C: ... 10 | 11 | -------------------------------------------------------------------------------- /examples/mixed_sub/python/mixed_sub/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jij-Inc/pyo3-stub-gen/5e772fe386df68fb764b98cf8eb7771194a630a7/examples/mixed_sub/python/mixed_sub/py.typed -------------------------------------------------------------------------------- /examples/mixed_sub/src/bin/stub_gen.rs: -------------------------------------------------------------------------------- 1 | use pyo3_stub_gen::Result; 2 | 3 | fn main() -> Result<()> { 4 | env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init(); 5 | let stub = mixed_sub::stub_info()?; 6 | stub.generate()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /examples/mixed_sub/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3_stub_gen::{define_stub_info_gatherer, derive::*}; 3 | 4 | // Specify the module name explicitly 5 | #[gen_stub_pyclass] 6 | #[pyclass(module = "mixed_sub.main_mod")] 7 | #[derive(Debug)] 8 | struct A { 9 | x: usize, 10 | } 11 | 12 | #[gen_stub_pymethods] 13 | #[pymethods] 14 | impl A { 15 | fn show_x(&self) { 16 | println!("x = {}", self.x); 17 | } 18 | } 19 | 20 | #[gen_stub_pyfunction(module = "mixed_sub.main_mod")] 21 | #[pyfunction] 22 | fn create_a(x: usize) -> A { 23 | A { x } 24 | } 25 | 26 | // Do not specify the module name explicitly 27 | // This will be placed in the main module 28 | #[gen_stub_pyclass] 29 | #[pyclass] 30 | #[derive(Debug)] 31 | struct B { 32 | x: usize, 33 | } 34 | 35 | #[gen_stub_pymethods] 36 | #[pymethods] 37 | impl B { 38 | fn show_x(&self) { 39 | println!("x = {}", self.x); 40 | } 41 | } 42 | 43 | #[gen_stub_pyfunction] 44 | #[pyfunction] 45 | fn create_b(x: usize) -> B { 46 | B { x } 47 | } 48 | 49 | // Class in submodule 50 | #[gen_stub_pyclass] 51 | #[pyclass(module = "mixed_sub.main_mod.sub_mod")] 52 | #[derive(Debug)] 53 | struct C { 54 | x: usize, 55 | } 56 | 57 | #[gen_stub_pymethods] 58 | #[pymethods] 59 | impl C { 60 | fn show_x(&self) { 61 | println!("x = {}", self.x); 62 | } 63 | } 64 | 65 | #[gen_stub_pyfunction(module = "mixed_sub.main_mod.sub_mod")] 66 | #[pyfunction] 67 | fn create_c(x: usize) -> C { 68 | C { x } 69 | } 70 | 71 | #[gen_stub_pyfunction(module = "mixed_sub.main_mod.int")] 72 | #[pyfunction] 73 | fn dummy_int_fun(x: usize) -> usize { 74 | x 75 | } 76 | 77 | #[pymodule] 78 | fn main_mod(m: &Bound) -> PyResult<()> { 79 | m.add_class::()?; 80 | m.add_class::()?; 81 | m.add_function(wrap_pyfunction!(create_a, m)?)?; 82 | m.add_function(wrap_pyfunction!(create_b, m)?)?; 83 | sub_mod(m)?; 84 | int_mod(m)?; 85 | Ok(()) 86 | } 87 | 88 | fn sub_mod(parent: &Bound) -> PyResult<()> { 89 | let py = parent.py(); 90 | let sub = PyModule::new(py, "sub_mod")?; 91 | sub.add_class::()?; 92 | sub.add_function(wrap_pyfunction!(create_c, &sub)?)?; 93 | parent.add_submodule(&sub)?; 94 | Ok(()) 95 | } 96 | 97 | /// A dummy module to pollute namespace with unqualified `int` 98 | fn int_mod(parent: &Bound) -> PyResult<()> { 99 | let py = parent.py(); 100 | let sub = PyModule::new(py, "int")?; 101 | sub.add_function(wrap_pyfunction!(dummy_int_fun, &sub)?)?; 102 | parent.add_submodule(&sub)?; 103 | Ok(()) 104 | } 105 | 106 | define_stub_info_gatherer!(stub_info); 107 | 108 | /// Test of unit test for testing link problem 109 | #[cfg(test)] 110 | mod test { 111 | #[test] 112 | fn test() { 113 | assert_eq!(2 + 2, 4); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/mixed_sub/tests/test_mixed_sub.py: -------------------------------------------------------------------------------- 1 | from mixed_sub import main_mod 2 | 3 | 4 | def test_main_mod(): 5 | a = main_mod.create_a(1) 6 | a.show_x() 7 | 8 | b = main_mod.create_b(1) 9 | b.show_x() 10 | 11 | 12 | def test_sub_mod(): 13 | c = main_mod.sub_mod.create_c(1) 14 | c.show_x() 15 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mixed_sub_multiple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Example for Mixed Rust/Python layout with submodule" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | env_logger = "0.11.8" 12 | pyo3 = ">= 0.24.0" 13 | pyo3-stub-gen = { path = "../../pyo3-stub-gen" } 14 | 15 | [[bin]] 16 | name = "stub_gen" 17 | doc = false 18 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | # yaml-language-server: $schema=https://taskfile.dev/schema.json 3 | version: "3" 4 | 5 | tasks: 6 | stub-gen: 7 | desc: Generate stub file 8 | cmds: 9 | - cargo run --bin stub_gen 10 | 11 | generate-lockfile: 12 | desc: Generate lockfile 13 | cmds: 14 | - cargo generate-lockfile 15 | 16 | test: 17 | desc: Run tests 18 | cmds: 19 | - uv run pytest 20 | - uv run pyright 21 | - uvx ruff check 22 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.1,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "mixed_sub_multiple" 7 | version = "0.1" 8 | requires-python = ">=3.9" 9 | 10 | [tool.maturin] 11 | python-source = "python" 12 | module-name = "mixed_sub_multiple.main_mod" 13 | features = ["pyo3/extension-module"] 14 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/python/mixed_sub_multiple/main_mod/__init__.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | from . import mod_a 5 | from . import mod_b 6 | 7 | def greet_main() -> None: ... 8 | 9 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/python/mixed_sub_multiple/main_mod/mod_a.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | 5 | def greet_a() -> None: ... 6 | 7 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/python/mixed_sub_multiple/main_mod/mod_b.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | 5 | def greet_b() -> None: ... 6 | 7 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/python/mixed_sub_multiple/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jij-Inc/pyo3-stub-gen/5e772fe386df68fb764b98cf8eb7771194a630a7/examples/mixed_sub_multiple/python/mixed_sub_multiple/py.typed -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/src/bin/stub_gen.rs: -------------------------------------------------------------------------------- 1 | use pyo3_stub_gen::Result; 2 | 3 | fn main() -> Result<()> { 4 | env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init(); 5 | let stub = mixed_sub_multiple::stub_info()?; 6 | stub.generate()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3_stub_gen::{define_stub_info_gatherer, derive::*}; 3 | 4 | #[gen_stub_pyfunction(module = "mixed_sub_multiple.main_mod.mod_a")] 5 | #[pyfunction(name = "greet_a")] 6 | pub fn greet_a() { 7 | println!("Hello from mod_A!") 8 | } 9 | 10 | #[gen_stub_pyfunction(module = "mixed_sub_multiple.main_mod")] 11 | #[pyfunction(name = "greet_main")] 12 | pub fn greet_main() { 13 | println!("Hello from main_mod!") 14 | } 15 | 16 | #[gen_stub_pyfunction(module = "mixed_sub_multiple.main_mod.mod_b")] 17 | #[pyfunction(name = "greet_b")] 18 | pub fn greet_b() { 19 | println!("Hello from mod_B!") 20 | } 21 | 22 | #[pymodule] 23 | fn main_mod(m: &Bound) -> PyResult<()> { 24 | m.add_function(wrap_pyfunction!(greet_main, m)?)?; 25 | mod_a(m)?; 26 | mod_b(m)?; 27 | Ok(()) 28 | } 29 | 30 | #[pymodule] 31 | fn mod_a(parent: &Bound) -> PyResult<()> { 32 | let py = parent.py(); 33 | let sub = PyModule::new(py, "mod_a")?; 34 | sub.add_function(wrap_pyfunction!(greet_a, &sub)?)?; 35 | parent.add_submodule(&sub)?; 36 | Ok(()) 37 | } 38 | 39 | #[pymodule] 40 | fn mod_b(parent: &Bound) -> PyResult<()> { 41 | let py = parent.py(); 42 | let sub = PyModule::new(py, "mod_b")?; 43 | sub.add_function(wrap_pyfunction!(greet_b, &sub)?)?; 44 | parent.add_submodule(&sub)?; 45 | Ok(()) 46 | } 47 | 48 | define_stub_info_gatherer!(stub_info); 49 | 50 | /// Test of unit test for testing link problem 51 | #[cfg(test)] 52 | mod test { 53 | #[test] 54 | fn test() { 55 | assert_eq!(2 + 2, 4); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/mixed_sub_multiple/tests/test_mixed_sub_multiple.py: -------------------------------------------------------------------------------- 1 | from mixed_sub_multiple import main_mod 2 | 3 | 4 | def test_main_mod(): 5 | main_mod.greet_main() 6 | 7 | 8 | def test_sub_mod_a(): 9 | main_mod.mod_a.greet_a() 10 | 11 | 12 | def test_sub_mod_b(): 13 | main_mod.mod_b.greet_b() 14 | -------------------------------------------------------------------------------- /examples/pure/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Since the Python C API for ABI3 is called "Limited API", 2 | # we use "unlimited" to indicate that the API is not limited to ABI3. 3 | 4 | [package] 5 | name = "pure" 6 | edition = "2021" 7 | version = "0.1.0" 8 | description = "Example for pure-Rust layout with non-ABI3 Python C API" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | ahash = "0.8.11" 15 | env_logger = "0.11.8" 16 | pyo3-stub-gen = { path = "../../pyo3-stub-gen" } 17 | pyo3 = ">= 0.24.0" 18 | 19 | [[bin]] 20 | name = "stub_gen" 21 | doc = false 22 | -------------------------------------------------------------------------------- /examples/pure/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | # yaml-language-server: $schema=https://taskfile.dev/schema.json 3 | version: "3" 4 | 5 | tasks: 6 | stub-gen: 7 | desc: Generate stub file 8 | cmds: 9 | - cargo run --bin stub_gen 10 | 11 | generate-lockfile: 12 | desc: Generate lockfile 13 | cmds: 14 | - cargo generate-lockfile 15 | 16 | test: 17 | desc: Run tests 18 | cmds: 19 | - uv run pytest 20 | - uv run pyright 21 | - uvx ruff check 22 | -------------------------------------------------------------------------------- /examples/pure/pure.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | import builtins 5 | import datetime 6 | import os 7 | import pathlib 8 | import typing 9 | from enum import Enum 10 | 11 | MY_CONSTANT: builtins.int 12 | class A: 13 | x: builtins.int 14 | def __new__(cls, x:builtins.int) -> A: 15 | r""" 16 | This is a constructor of :class:`A`. 17 | """ 18 | def show_x(self) -> None: ... 19 | def ref_test(self, x:dict) -> dict: ... 20 | 21 | class B(A): 22 | ... 23 | 24 | class MyDate(datetime.date): 25 | ... 26 | 27 | class Number(Enum): 28 | FLOAT = ... 29 | INTEGER = ... 30 | 31 | is_float: builtins.bool 32 | r""" 33 | Whether the number is a float. 34 | """ 35 | 36 | is_integer: builtins.bool 37 | r""" 38 | Whether the number is an integer. 39 | """ 40 | 41 | class NumberRenameAll(Enum): 42 | FLOAT = ... 43 | r""" 44 | Float variant 45 | """ 46 | INTEGER = ... 47 | 48 | def ahash_dict() -> builtins.dict[builtins.str, builtins.int]: ... 49 | 50 | def create_a(x:builtins.int=2) -> A: ... 51 | 52 | def create_dict(n:builtins.int) -> builtins.dict[builtins.int, builtins.list[builtins.int]]: ... 53 | 54 | def default_value(num:Number=Number.FLOAT) -> Number: ... 55 | 56 | def echo_path(path:builtins.str | os.PathLike | pathlib.Path) -> pathlib.Path: ... 57 | 58 | def print_c(c:typing.Optional[builtins.int]=None) -> None: ... 59 | 60 | def read_dict(dict:typing.Mapping[builtins.int, typing.Mapping[builtins.int, builtins.int]]) -> None: ... 61 | 62 | def str_len(x:builtins.str) -> builtins.int: 63 | r""" 64 | Returns the length of the string. 65 | """ 66 | 67 | def sum(v:typing.Sequence[builtins.int]) -> builtins.int: 68 | r""" 69 | Returns the sum of two numbers as a string. 70 | """ 71 | 72 | class MyError(RuntimeError): ... 73 | 74 | -------------------------------------------------------------------------------- /examples/pure/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.1,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "pure" 7 | version = "0.1" 8 | requires-python = ">=3.9" 9 | 10 | [tool.maturin] 11 | features = ["pyo3/extension-module"] 12 | -------------------------------------------------------------------------------- /examples/pure/src/bin/stub_gen.rs: -------------------------------------------------------------------------------- 1 | use pyo3_stub_gen::Result; 2 | 3 | fn main() -> Result<()> { 4 | env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init(); 5 | let stub = pure::stub_info()?; 6 | stub.generate()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /examples/pure/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(target_os = "macos", doc = include_str!("../../../README.md"))] 2 | mod readme {} 3 | 4 | use ahash::RandomState; 5 | use pyo3::{exceptions::PyRuntimeError, prelude::*, types::*}; 6 | use pyo3_stub_gen::{create_exception, define_stub_info_gatherer, derive::*, module_variable}; 7 | use std::{collections::HashMap, path::PathBuf}; 8 | 9 | /// Returns the sum of two numbers as a string. 10 | #[gen_stub_pyfunction] 11 | #[pyfunction] 12 | fn sum(v: Vec) -> u32 { 13 | v.iter().sum() 14 | } 15 | 16 | #[gen_stub_pyfunction] 17 | #[pyfunction] 18 | fn read_dict(dict: HashMap>) { 19 | for (k, v) in dict { 20 | for (k2, v2) in v { 21 | println!("{} {} {}", k, k2, v2); 22 | } 23 | } 24 | } 25 | 26 | #[gen_stub_pyfunction] 27 | #[pyfunction] 28 | fn create_dict(n: usize) -> HashMap> { 29 | let mut dict = HashMap::new(); 30 | for i in 0..n { 31 | dict.insert(i, (0..i).collect()); 32 | } 33 | dict 34 | } 35 | 36 | #[gen_stub_pyclass] 37 | #[pyclass(extends=PyDate)] 38 | struct MyDate; 39 | 40 | #[gen_stub_pyclass] 41 | #[pyclass(subclass)] 42 | #[derive(Debug)] 43 | struct A { 44 | #[pyo3(get, set)] 45 | x: usize, 46 | } 47 | 48 | #[gen_stub_pymethods] 49 | #[pymethods] 50 | impl A { 51 | /// This is a constructor of :class:`A`. 52 | #[new] 53 | fn new(x: usize) -> Self { 54 | Self { x } 55 | } 56 | 57 | fn show_x(&self) { 58 | println!("x = {}", self.x); 59 | } 60 | 61 | fn ref_test<'a>(&self, x: Bound<'a, PyDict>) -> Bound<'a, PyDict> { 62 | x 63 | } 64 | } 65 | 66 | #[gen_stub_pyfunction] 67 | #[pyfunction] 68 | #[pyo3(signature = (x = 2))] 69 | fn create_a(x: usize) -> A { 70 | A { x } 71 | } 72 | 73 | #[gen_stub_pyclass] 74 | #[pyclass(extends=A)] 75 | #[derive(Debug)] 76 | struct B; 77 | 78 | /// `C` only impl `FromPyObject` 79 | #[derive(Debug)] 80 | struct C { 81 | x: usize, 82 | } 83 | #[gen_stub_pyfunction] 84 | #[pyfunction(signature = (c=None))] 85 | fn print_c(c: Option) { 86 | if let Some(c) = c { 87 | println!("{}", c.x); 88 | } else { 89 | println!("None"); 90 | } 91 | } 92 | impl FromPyObject<'_> for C { 93 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { 94 | Ok(C { x: ob.extract()? }) 95 | } 96 | } 97 | impl pyo3_stub_gen::PyStubType for C { 98 | fn type_output() -> pyo3_stub_gen::TypeInfo { 99 | usize::type_output() 100 | } 101 | } 102 | 103 | create_exception!(pure, MyError, PyRuntimeError); 104 | 105 | /// Returns the length of the string. 106 | #[gen_stub_pyfunction] 107 | #[pyfunction] 108 | fn str_len(x: &str) -> PyResult { 109 | Ok(x.len()) 110 | } 111 | 112 | #[gen_stub_pyfunction] 113 | #[pyfunction] 114 | fn echo_path(path: PathBuf) -> PyResult { 115 | Ok(path) 116 | } 117 | 118 | #[gen_stub_pyfunction] 119 | #[pyfunction] 120 | fn ahash_dict() -> HashMap { 121 | let mut map: HashMap = HashMap::with_hasher(RandomState::new()); 122 | map.insert("apple".to_string(), 3); 123 | map.insert("banana".to_string(), 2); 124 | map.insert("orange".to_string(), 5); 125 | map 126 | } 127 | 128 | #[gen_stub_pyclass_enum] 129 | #[pyclass(eq, eq_int)] 130 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 131 | pub enum Number { 132 | #[pyo3(name = "FLOAT")] 133 | Float, 134 | #[pyo3(name = "INTEGER")] 135 | Integer, 136 | } 137 | 138 | #[gen_stub_pyclass_enum] 139 | #[pyclass(eq, eq_int)] 140 | #[pyo3(rename_all = "UPPERCASE")] 141 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 142 | pub enum NumberRenameAll { 143 | /// Float variant 144 | Float, 145 | Integer, 146 | } 147 | 148 | #[gen_stub_pymethods] 149 | #[pymethods] 150 | impl Number { 151 | #[getter] 152 | /// Whether the number is a float. 153 | fn is_float(&self) -> bool { 154 | matches!(self, Self::Float) 155 | } 156 | 157 | #[getter] 158 | /// Whether the number is an integer. 159 | fn is_integer(&self) -> bool { 160 | matches!(self, Self::Integer) 161 | } 162 | } 163 | 164 | module_variable!("pure", "MY_CONSTANT", usize); 165 | 166 | // Test if non-any PyObject Target can be a default value 167 | #[gen_stub_pyfunction] 168 | #[pyfunction] 169 | #[pyo3(signature = (num = Number::Float))] 170 | fn default_value(num: Number) -> Number { 171 | num 172 | } 173 | 174 | /// Initializes the Python module 175 | #[pymodule] 176 | fn pure(m: &Bound) -> PyResult<()> { 177 | m.add("MyError", m.py().get_type::())?; 178 | m.add("MY_CONSTANT", 19937)?; 179 | m.add_class::()?; 180 | m.add_class::()?; 181 | m.add_class::()?; 182 | m.add_class::()?; 183 | m.add_class::()?; 184 | m.add_function(wrap_pyfunction!(sum, m)?)?; 185 | m.add_function(wrap_pyfunction!(create_dict, m)?)?; 186 | m.add_function(wrap_pyfunction!(read_dict, m)?)?; 187 | m.add_function(wrap_pyfunction!(create_a, m)?)?; 188 | m.add_function(wrap_pyfunction!(print_c, m)?)?; 189 | m.add_function(wrap_pyfunction!(str_len, m)?)?; 190 | m.add_function(wrap_pyfunction!(echo_path, m)?)?; 191 | m.add_function(wrap_pyfunction!(ahash_dict, m)?)?; 192 | m.add_function(wrap_pyfunction!(default_value, m)?)?; 193 | Ok(()) 194 | } 195 | 196 | define_stub_info_gatherer!(stub_info); 197 | 198 | /// Test of unit test for testing link problem 199 | #[cfg(test)] 200 | mod test { 201 | #[test] 202 | fn test() { 203 | assert_eq!(2 + 2, 4); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /examples/pure/tests/test_python.py: -------------------------------------------------------------------------------- 1 | from pure import sum, create_dict, read_dict, echo_path, ahash_dict 2 | import pytest 3 | import pathlib 4 | 5 | 6 | def test_sum(): 7 | assert sum([1, 2]) == 3 8 | assert sum((1, 2)) == 3 9 | 10 | 11 | def test_create_dict(): 12 | assert create_dict(3) == {0: [], 1: [0], 2: [0, 1]} 13 | 14 | 15 | def test_ahash_dict(): 16 | assert ahash_dict() == {"apple": 3, "banana": 2, "orange": 5} 17 | 18 | 19 | def test_read_dict(): 20 | read_dict( 21 | { 22 | 0: { 23 | 0: 1, 24 | }, 25 | 1: { 26 | 0: 2, 27 | 1: 3, 28 | }, 29 | } 30 | ) 31 | 32 | with pytest.raises(TypeError) as e: 33 | read_dict({0: 1}) # type: ignore 34 | assert ( 35 | str(e.value) == "argument 'dict': 'int' object cannot be converted to 'PyDict'" 36 | ) 37 | 38 | 39 | def test_path(): 40 | out = echo_path(pathlib.Path("test")) 41 | assert out == pathlib.Path("test") 42 | 43 | out = echo_path("test") 44 | assert out == pathlib.Path("test") 45 | -------------------------------------------------------------------------------- /examples/pure_abi3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pure_abi3" 3 | edition = "2021" 4 | version = "0.1.0" 5 | description = "Example for pure-Rust layout with ABI3" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | ahash = "0.8.11" 12 | env_logger = "0.11.8" 13 | # Use 0.24 since the issue #184 is automatically fixed in 0.25 14 | pyo3 = { version = "0.24.0", features = ["abi3-py39"] } 15 | pyo3-stub-gen = { path = "../../pyo3-stub-gen" } 16 | 17 | [[bin]] 18 | name = "stub_gen" 19 | doc = false 20 | -------------------------------------------------------------------------------- /examples/pure_abi3/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | # yaml-language-server: $schema=https://taskfile.dev/schema.json 3 | version: "3" 4 | 5 | tasks: 6 | stub-gen: 7 | desc: Generate stub file 8 | cmds: 9 | - cargo run --bin stub_gen 10 | 11 | generate-lockfile: 12 | desc: Generate lockfile 13 | cmds: 14 | - cargo generate-lockfile 15 | 16 | test: 17 | desc: Run tests 18 | cmds: 19 | - uv run pytest 20 | - uv run pyright 21 | - uvx ruff check 22 | -------------------------------------------------------------------------------- /examples/pure_abi3/pure_abi3.pyi: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by pyo3_stub_gen 2 | # ruff: noqa: E501, F401 3 | 4 | import builtins 5 | import os 6 | import pathlib 7 | import typing 8 | from enum import Enum 9 | 10 | MY_CONSTANT: builtins.int 11 | class A: 12 | x: builtins.int 13 | def __new__(cls, x:builtins.int) -> A: 14 | r""" 15 | This is a constructor of :class:`A`. 16 | """ 17 | def show_x(self) -> None: ... 18 | def ref_test(self, x:dict) -> dict: ... 19 | 20 | class B(A): 21 | ... 22 | 23 | class Number(Enum): 24 | FLOAT = ... 25 | INTEGER = ... 26 | 27 | is_float: builtins.bool 28 | r""" 29 | Whether the number is a float. 30 | """ 31 | 32 | is_integer: builtins.bool 33 | r""" 34 | Whether the number is an integer. 35 | """ 36 | 37 | class NumberRenameAll(Enum): 38 | FLOAT = ... 39 | INTEGER = ... 40 | 41 | def ahash_dict() -> builtins.dict[builtins.str, builtins.int]: ... 42 | 43 | def create_a(x:builtins.int=2) -> A: ... 44 | 45 | def create_dict(n:builtins.int) -> builtins.dict[builtins.int, builtins.list[builtins.int]]: ... 46 | 47 | def default_value(num:Number=Number.FLOAT) -> Number: ... 48 | 49 | def echo_path(path:builtins.str | os.PathLike | pathlib.Path) -> pathlib.Path: ... 50 | 51 | def read_dict(dict:typing.Mapping[builtins.int, typing.Mapping[builtins.int, builtins.int]]) -> None: ... 52 | 53 | def str_len(x:builtins.str) -> builtins.int: 54 | r""" 55 | Returns the length of the string. 56 | """ 57 | 58 | def sum(v:typing.Sequence[builtins.int]) -> builtins.int: 59 | r""" 60 | Returns the sum of two numbers as a string. 61 | """ 62 | 63 | class MyError(RuntimeError): ... 64 | 65 | -------------------------------------------------------------------------------- /examples/pure_abi3/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.1,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "pure_abi3" 7 | version = "0.1" 8 | requires-python = ">=3.9" 9 | 10 | [tool.maturin] 11 | features = ["pyo3/extension-module"] 12 | -------------------------------------------------------------------------------- /examples/pure_abi3/src/bin/stub_gen.rs: -------------------------------------------------------------------------------- 1 | use pyo3_stub_gen::Result; 2 | 3 | fn main() -> Result<()> { 4 | env_logger::Builder::from_env(env_logger::Env::default().filter_or("RUST_LOG", "info")).init(); 5 | let stub = pure_abi3::stub_info()?; 6 | stub.generate()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /examples/pure_abi3/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(target_os = "macos", doc = include_str!("../../../README.md"))] 2 | mod readme {} 3 | 4 | use ahash::RandomState; 5 | use pyo3::{exceptions::PyRuntimeError, prelude::*, types::*}; 6 | use pyo3_stub_gen::{create_exception, define_stub_info_gatherer, derive::*, module_variable}; 7 | use std::{collections::HashMap, path::PathBuf}; 8 | 9 | /// Returns the sum of two numbers as a string. 10 | #[gen_stub_pyfunction] 11 | #[pyfunction] 12 | fn sum(v: Vec) -> u32 { 13 | v.iter().sum() 14 | } 15 | 16 | #[gen_stub_pyfunction] 17 | #[pyfunction] 18 | fn read_dict(dict: HashMap>) { 19 | for (k, v) in dict { 20 | for (k2, v2) in v { 21 | println!("{} {} {}", k, k2, v2); 22 | } 23 | } 24 | } 25 | 26 | #[gen_stub_pyfunction] 27 | #[pyfunction] 28 | fn create_dict(n: usize) -> HashMap> { 29 | let mut dict = HashMap::new(); 30 | for i in 0..n { 31 | dict.insert(i, (0..i).collect()); 32 | } 33 | dict 34 | } 35 | 36 | #[gen_stub_pyclass] 37 | #[pyclass(subclass)] 38 | #[derive(Debug)] 39 | struct A { 40 | #[pyo3(get, set)] 41 | x: usize, 42 | } 43 | 44 | #[gen_stub_pymethods] 45 | #[pymethods] 46 | impl A { 47 | /// This is a constructor of :class:`A`. 48 | #[new] 49 | fn new(x: usize) -> Self { 50 | Self { x } 51 | } 52 | 53 | fn show_x(&self) { 54 | println!("x = {}", self.x); 55 | } 56 | 57 | fn ref_test<'a>(&self, x: Bound<'a, PyDict>) -> Bound<'a, PyDict> { 58 | x 59 | } 60 | } 61 | 62 | #[gen_stub_pyfunction] 63 | #[pyfunction] 64 | #[pyo3(signature = (x = 2))] 65 | fn create_a(x: usize) -> A { 66 | A { x } 67 | } 68 | 69 | #[gen_stub_pyclass] 70 | #[pyclass(extends=A)] 71 | #[derive(Debug)] 72 | struct B; 73 | 74 | create_exception!(pure_abi3, MyError, PyRuntimeError); 75 | 76 | /// Returns the length of the string. 77 | #[gen_stub_pyfunction] 78 | #[pyfunction] 79 | fn str_len(x: &str) -> PyResult { 80 | Ok(x.len()) 81 | } 82 | 83 | #[gen_stub_pyfunction] 84 | #[pyfunction] 85 | fn echo_path(path: PathBuf) -> PyResult { 86 | Ok(path) 87 | } 88 | 89 | #[gen_stub_pyfunction] 90 | #[pyfunction] 91 | fn ahash_dict() -> HashMap { 92 | let mut map: HashMap = HashMap::with_hasher(RandomState::new()); 93 | map.insert("apple".to_string(), 3); 94 | map.insert("banana".to_string(), 2); 95 | map.insert("orange".to_string(), 5); 96 | map 97 | } 98 | 99 | #[gen_stub_pyclass_enum] 100 | #[pyclass(eq, eq_int)] 101 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 102 | pub enum Number { 103 | #[pyo3(name = "FLOAT")] 104 | Float, 105 | #[pyo3(name = "INTEGER")] 106 | Integer, 107 | } 108 | 109 | #[gen_stub_pyclass_enum] 110 | #[pyclass(eq, eq_int)] 111 | #[pyo3(rename_all = "UPPERCASE")] 112 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 113 | pub enum NumberRenameAll { 114 | Float, 115 | Integer, 116 | } 117 | 118 | #[gen_stub_pymethods] 119 | #[pymethods] 120 | impl Number { 121 | #[getter] 122 | /// Whether the number is a float. 123 | fn is_float(&self) -> bool { 124 | matches!(self, Self::Float) 125 | } 126 | 127 | #[getter] 128 | /// Whether the number is an integer. 129 | fn is_integer(&self) -> bool { 130 | matches!(self, Self::Integer) 131 | } 132 | } 133 | 134 | module_variable!("pure_abi3", "MY_CONSTANT", usize); 135 | 136 | // Test if non-any PyObject Target can be a default value 137 | #[gen_stub_pyfunction] 138 | #[pyfunction] 139 | #[pyo3(signature = (num = Number::Float))] 140 | fn default_value(num: Number) -> Number { 141 | num 142 | } 143 | 144 | /// Initializes the Python module 145 | #[pymodule] 146 | fn pure_abi3(m: &Bound) -> PyResult<()> { 147 | m.add("MyError", m.py().get_type::())?; 148 | m.add("MY_CONSTANT", 19937)?; 149 | m.add_class::()?; 150 | m.add_class::()?; 151 | m.add_class::()?; 152 | m.add_class::()?; 153 | m.add_function(wrap_pyfunction!(sum, m)?)?; 154 | m.add_function(wrap_pyfunction!(create_dict, m)?)?; 155 | m.add_function(wrap_pyfunction!(read_dict, m)?)?; 156 | m.add_function(wrap_pyfunction!(create_a, m)?)?; 157 | m.add_function(wrap_pyfunction!(str_len, m)?)?; 158 | m.add_function(wrap_pyfunction!(echo_path, m)?)?; 159 | m.add_function(wrap_pyfunction!(ahash_dict, m)?)?; 160 | m.add_function(wrap_pyfunction!(default_value, m)?)?; 161 | Ok(()) 162 | } 163 | 164 | define_stub_info_gatherer!(stub_info); 165 | 166 | /// Test of unit test for testing link problem 167 | #[cfg(test)] 168 | mod test { 169 | #[test] 170 | fn test() { 171 | assert_eq!(2 + 2, 4); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /examples/pure_abi3/tests/test_python.py: -------------------------------------------------------------------------------- 1 | from pure_abi3 import sum, create_dict, read_dict, echo_path, ahash_dict 2 | import pytest 3 | import pathlib 4 | 5 | 6 | def test_sum(): 7 | assert sum([1, 2]) == 3 8 | assert sum((1, 2)) == 3 9 | 10 | 11 | def test_create_dict(): 12 | assert create_dict(3) == {0: [], 1: [0], 2: [0, 1]} 13 | 14 | 15 | def test_ahash_dict(): 16 | assert ahash_dict() == {"apple": 3, "banana": 2, "orange": 5} 17 | 18 | 19 | def test_read_dict(): 20 | read_dict( 21 | { 22 | 0: { 23 | 0: 1, 24 | }, 25 | 1: { 26 | 0: 2, 27 | 1: 3, 28 | }, 29 | } 30 | ) 31 | 32 | with pytest.raises(TypeError) as e: 33 | read_dict({0: 1}) # type: ignore 34 | assert ( 35 | str(e.value) == "argument 'dict': 'int' object cannot be converted to 'PyDict'" 36 | ) 37 | 38 | 39 | def test_path(): 40 | out = echo_path(pathlib.Path("test")) 41 | assert out == pathlib.Path("test") 42 | 43 | out = echo_path("test") 44 | assert out == pathlib.Path("test") 45 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyo3-stub-gen-derive" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | description.workspace = true 7 | repository.workspace = true 8 | keywords.workspace = true 9 | license.workspace = true 10 | readme.workspace = true 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | heck.workspace = true 17 | proc-macro2.workspace = true 18 | quote.workspace = true 19 | syn = { workspace = true, features = ["full", "extra-traits"] } 20 | 21 | [dev-dependencies] 22 | pyo3-stub-gen = { path = "../pyo3-stub-gen" } 23 | insta.workspace = true 24 | inventory.workspace = true 25 | prettyplease.workspace = true 26 | pyo3.workspace = true 27 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub.rs: -------------------------------------------------------------------------------- 1 | //! Code generation for embedding metadata for generating Python stub file. 2 | //! 3 | //! These metadata are embedded as `inventory::submit!` block like: 4 | //! 5 | //! ```rust 6 | //! # use pyo3::*; 7 | //! # use pyo3_stub_gen::type_info::*; 8 | //! # struct PyPlaceholder; 9 | //! inventory::submit!{ 10 | //! PyClassInfo { 11 | //! pyclass_name: "Placeholder", 12 | //! module: Some("my_module"), 13 | //! struct_id: std::any::TypeId::of::, 14 | //! members: &[ 15 | //! MemberInfo { 16 | //! name: "name", 17 | //! r#type: ::type_output, 18 | //! doc: "", 19 | //! }, 20 | //! MemberInfo { 21 | //! name: "ndim", 22 | //! r#type: ::type_output, 23 | //! doc: "", 24 | //! }, 25 | //! MemberInfo { 26 | //! name: "description", 27 | //! r#type: as ::pyo3_stub_gen::PyStubType>::type_output, 28 | //! doc: "", 29 | //! }, 30 | //! ], 31 | //! doc: "", 32 | //! bases: &[], 33 | //! } 34 | //! } 35 | //! ``` 36 | //! 37 | //! and this submodule responsible for generating such codes from Rust code like 38 | //! 39 | //! ```rust 40 | //! # use pyo3::*; 41 | //! #[pyclass(mapping, module = "my_module", name = "Placeholder")] 42 | //! #[derive(Debug, Clone)] 43 | //! pub struct PyPlaceholder { 44 | //! #[pyo3(get)] 45 | //! pub name: String, 46 | //! #[pyo3(get)] 47 | //! pub ndim: usize, 48 | //! #[pyo3(get)] 49 | //! pub description: Option, 50 | //! pub custom_latex: Option, 51 | //! } 52 | //! ``` 53 | //! 54 | //! Mechanism 55 | //! ---------- 56 | //! Code generation will take three steps: 57 | //! 58 | //! 1. Parse input [proc_macro2::TokenStream] into corresponding syntax tree component in [syn], 59 | //! - e.g. [ItemStruct] for `#[pyclass]`, [ItemImpl] for `#[pymethods]`, and so on. 60 | //! 2. Convert syntax tree components into `*Info` struct using [TryInto]. 61 | //! - e.g. [PyClassInfo] is converted from [ItemStruct], [PyMethodsInfo] is converted from [ItemImpl], and so on. 62 | //! 3. Generate token streams using implementation of [quote::ToTokens] trait for `*Info` structs. 63 | //! - [quote::quote!] macro uses this trait. 64 | //! 65 | 66 | mod arg; 67 | mod attr; 68 | mod member; 69 | mod method; 70 | mod pyclass; 71 | mod pyclass_enum; 72 | mod pyfunction; 73 | mod pymethods; 74 | mod renaming; 75 | mod signature; 76 | mod stub_type; 77 | mod util; 78 | 79 | use arg::*; 80 | use attr::*; 81 | use member::*; 82 | use method::*; 83 | use pyclass::*; 84 | use pyclass_enum::*; 85 | use pyfunction::*; 86 | use pymethods::*; 87 | use renaming::*; 88 | use signature::*; 89 | use stub_type::*; 90 | use util::*; 91 | 92 | use proc_macro2::TokenStream as TokenStream2; 93 | use quote::quote; 94 | use syn::{parse2, ItemEnum, ItemFn, ItemImpl, ItemStruct, Result}; 95 | 96 | pub fn pyclass(item: TokenStream2) -> Result { 97 | let inner = PyClassInfo::try_from(parse2::(item.clone())?)?; 98 | let derive_stub_type = StubType::from(&inner); 99 | Ok(quote! { 100 | #item 101 | #derive_stub_type 102 | pyo3_stub_gen::inventory::submit! { 103 | #inner 104 | } 105 | }) 106 | } 107 | 108 | pub fn pyclass_enum(item: TokenStream2) -> Result { 109 | let inner = PyEnumInfo::try_from(parse2::(item.clone())?)?; 110 | let derive_stub_type = StubType::from(&inner); 111 | Ok(quote! { 112 | #item 113 | #derive_stub_type 114 | pyo3_stub_gen::inventory::submit! { 115 | #inner 116 | } 117 | }) 118 | } 119 | 120 | pub fn pymethods(item: TokenStream2) -> Result { 121 | let inner = PyMethodsInfo::try_from(parse2::(item.clone())?)?; 122 | Ok(quote! { 123 | #item 124 | #[automatically_derived] 125 | pyo3_stub_gen::inventory::submit! { 126 | #inner 127 | } 128 | }) 129 | } 130 | 131 | pub fn pyfunction(attr: TokenStream2, item: TokenStream2) -> Result { 132 | let mut inner = PyFunctionInfo::try_from(parse2::(item.clone())?)?; 133 | inner.parse_attr(attr)?; 134 | Ok(quote! { 135 | #item 136 | #[automatically_derived] 137 | pyo3_stub_gen::inventory::submit! { 138 | #inner 139 | } 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/arg.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use syn::{ 3 | spanned::Spanned, FnArg, GenericArgument, PatType, PathArguments, Result, Type, TypePath, 4 | }; 5 | 6 | pub fn parse_args(iter: impl IntoIterator) -> Result> { 7 | let mut args = Vec::new(); 8 | for (n, arg) in iter.into_iter().enumerate() { 9 | if let FnArg::Receiver(_) = arg { 10 | continue; 11 | } 12 | let arg = ArgInfo::try_from(arg)?; 13 | if let Type::Path(TypePath { path, .. }) = &arg.r#type { 14 | let last = path.segments.last().unwrap(); 15 | if last.ident == "Python" { 16 | continue; 17 | } 18 | // Regard the first argument with `PyRef<'_, Self>` and `PyMutRef<'_, Self>` types as a receiver. 19 | if n == 0 && (last.ident == "PyRef" || last.ident == "PyRefMut") { 20 | if let PathArguments::AngleBracketed(inner) = &last.arguments { 21 | if let GenericArgument::Type(Type::Path(TypePath { path, .. })) = 22 | &inner.args[inner.args.len() - 1] 23 | { 24 | let last = path.segments.last().unwrap(); 25 | if last.ident == "Self" { 26 | continue; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | args.push(arg); 33 | } 34 | Ok(args) 35 | } 36 | 37 | #[derive(Debug)] 38 | pub struct ArgInfo { 39 | pub name: String, 40 | pub r#type: Type, 41 | } 42 | 43 | impl TryFrom for ArgInfo { 44 | type Error = syn::Error; 45 | fn try_from(value: FnArg) -> Result { 46 | let span = value.span(); 47 | if let FnArg::Typed(PatType { pat, ty, .. }) = value { 48 | if let syn::Pat::Ident(mut ident) = *pat { 49 | ident.mutability = None; 50 | let name = ident.to_token_stream().to_string(); 51 | return Ok(Self { name, r#type: *ty }); 52 | } 53 | } 54 | Err(syn::Error::new(span, "Expected typed argument")) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/attr.rs: -------------------------------------------------------------------------------- 1 | use super::{RenamingRule, Signature}; 2 | use proc_macro2::TokenTree; 3 | use quote::ToTokens; 4 | use syn::{Attribute, Expr, ExprLit, Ident, Lit, Meta, MetaList, Result, Type}; 5 | 6 | pub fn extract_documents(attrs: &[Attribute]) -> Vec { 7 | let mut docs = Vec::new(); 8 | for attr in attrs { 9 | // `#[doc = "..."]` case 10 | if attr.path().is_ident("doc") { 11 | if let Meta::NameValue(syn::MetaNameValue { 12 | value: 13 | Expr::Lit(ExprLit { 14 | lit: Lit::Str(doc), .. 15 | }), 16 | .. 17 | }) = &attr.meta 18 | { 19 | let doc = doc.value(); 20 | // Remove head space 21 | // 22 | // ``` 23 | // /// This is special document! 24 | // ^ This space is trimmed here 25 | // ``` 26 | docs.push(if !doc.is_empty() && doc.starts_with(' ') { 27 | doc[1..].to_string() 28 | } else { 29 | doc 30 | }); 31 | } 32 | } 33 | } 34 | docs 35 | } 36 | 37 | /// `#[pyo3(...)]` style attributes appear in `#[pyclass]` and `#[pymethods]` proc-macros 38 | /// 39 | /// As the reference of PyO3 says: 40 | /// 41 | /// https://docs.rs/pyo3/latest/pyo3/attr.pyclass.html 42 | /// > All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, 43 | /// > or as one or more accompanying `#[pyo3(...)]` annotations, 44 | /// 45 | /// `#[pyclass(name = "MyClass", module = "MyModule")]` will be decomposed into 46 | /// `#[pyclass]` + `#[pyo3(name = "MyClass")]` + `#[pyo3(module = "MyModule")]`, 47 | /// i.e. two `Attr`s will be created for this case. 48 | /// 49 | #[derive(Debug, Clone, PartialEq)] 50 | pub enum Attr { 51 | // Attributes appears in `#[pyo3(...)]` form or its equivalence 52 | Name(String), 53 | Get, 54 | GetAll, 55 | Module(String), 56 | Signature(Signature), 57 | RenameAll(RenamingRule), 58 | Extends(Type), 59 | 60 | // Attributes appears in components within `#[pymethods]` 61 | // 62 | New, 63 | Getter(Option), 64 | StaticMethod, 65 | ClassMethod, 66 | } 67 | 68 | pub fn parse_pyo3_attrs(attrs: &[Attribute]) -> Result> { 69 | let mut out = Vec::new(); 70 | for attr in attrs { 71 | let mut new = parse_pyo3_attr(attr)?; 72 | out.append(&mut new); 73 | } 74 | Ok(out) 75 | } 76 | 77 | pub fn parse_pyo3_attr(attr: &Attribute) -> Result> { 78 | let mut pyo3_attrs = Vec::new(); 79 | let path = attr.path(); 80 | let is_full_path_pyo3_attr = path.segments.len() == 2 81 | && path 82 | .segments 83 | .first() 84 | .is_some_and(|seg| seg.ident.eq("pyo3")) 85 | && path.segments.last().is_some_and(|seg| { 86 | seg.ident.eq("pyclass") || seg.ident.eq("pymethods") || seg.ident.eq("pyfunction") 87 | }); 88 | if path.is_ident("pyclass") 89 | || path.is_ident("pymethods") 90 | || path.is_ident("pyfunction") 91 | || path.is_ident("pyo3") 92 | || is_full_path_pyo3_attr 93 | { 94 | // Inner tokens of `#[pyo3(...)]` may not be nested meta 95 | // which can be parsed by `Attribute::parse_nested_meta` 96 | // due to the case of `#[pyo3(signature = (...))]`. 97 | // https://pyo3.rs/v0.19.1/function/signature 98 | if let Meta::List(MetaList { tokens, .. }) = &attr.meta { 99 | use TokenTree::*; 100 | let tokens: Vec = tokens.clone().into_iter().collect(); 101 | // Since `(...)` part with `signature` becomes `TokenTree::Group`, 102 | // we can split entire stream by `,` first, and then pattern match to each cases. 103 | for tt in tokens.split(|tt| { 104 | if let Punct(p) = tt { 105 | p.as_char() == ',' 106 | } else { 107 | false 108 | } 109 | }) { 110 | match tt { 111 | [Ident(ident)] => { 112 | if ident == "get" { 113 | pyo3_attrs.push(Attr::Get); 114 | } 115 | if ident == "get_all" { 116 | pyo3_attrs.push(Attr::GetAll); 117 | } 118 | } 119 | [Ident(ident), Punct(_), Literal(lit)] => { 120 | if ident == "name" { 121 | pyo3_attrs 122 | .push(Attr::Name(lit.to_string().trim_matches('"').to_string())); 123 | } 124 | if ident == "module" { 125 | pyo3_attrs 126 | .push(Attr::Module(lit.to_string().trim_matches('"').to_string())); 127 | } 128 | if ident == "rename_all" { 129 | let name = lit.to_string().trim_matches('"').to_string(); 130 | if let Some(renaming_rule) = RenamingRule::try_new(&name) { 131 | pyo3_attrs.push(Attr::RenameAll(renaming_rule)); 132 | } 133 | } 134 | } 135 | [Ident(ident), Punct(_), Group(group)] => { 136 | if ident == "signature" { 137 | pyo3_attrs.push(Attr::Signature(syn::parse2(group.to_token_stream())?)); 138 | } 139 | } 140 | [Ident(ident), Punct(_), Ident(ident2)] => { 141 | if ident == "extends" { 142 | pyo3_attrs.push(Attr::Extends(syn::parse2(ident2.to_token_stream())?)); 143 | } 144 | } 145 | _ => {} 146 | } 147 | } 148 | } 149 | } else if path.is_ident("new") { 150 | pyo3_attrs.push(Attr::New); 151 | } else if path.is_ident("staticmethod") { 152 | pyo3_attrs.push(Attr::StaticMethod); 153 | } else if path.is_ident("classmethod") { 154 | pyo3_attrs.push(Attr::ClassMethod); 155 | } else if path.is_ident("getter") { 156 | if let Ok(inner) = attr.parse_args::() { 157 | pyo3_attrs.push(Attr::Getter(Some(inner.to_string()))); 158 | } else { 159 | pyo3_attrs.push(Attr::Getter(None)); 160 | } 161 | } 162 | 163 | Ok(pyo3_attrs) 164 | } 165 | 166 | #[cfg(test)] 167 | mod test { 168 | use super::*; 169 | use syn::{parse_str, Fields, ItemStruct}; 170 | 171 | #[test] 172 | fn test_parse_pyo3_attr() -> Result<()> { 173 | let item: ItemStruct = parse_str( 174 | r#" 175 | #[pyclass(mapping, module = "my_module", name = "Placeholder")] 176 | #[pyo3(rename_all = "SCREAMING_SNAKE_CASE")] 177 | pub struct PyPlaceholder { 178 | #[pyo3(get)] 179 | pub name: String, 180 | } 181 | "#, 182 | )?; 183 | // `#[pyclass]` part 184 | let attrs = parse_pyo3_attrs(&item.attrs)?; 185 | assert_eq!( 186 | attrs, 187 | vec![ 188 | Attr::Module("my_module".to_string()), 189 | Attr::Name("Placeholder".to_string()), 190 | Attr::RenameAll(RenamingRule::ScreamingSnakeCase), 191 | ] 192 | ); 193 | 194 | // `#[pyo3(get)]` part 195 | if let Fields::Named(fields) = item.fields { 196 | let attrs = parse_pyo3_attr(&fields.named[0].attrs[0])?; 197 | assert_eq!(attrs, vec![Attr::Get]); 198 | } else { 199 | unreachable!() 200 | } 201 | Ok(()) 202 | } 203 | 204 | #[test] 205 | fn test_parse_pyo3_attr_full_path() -> Result<()> { 206 | let item: ItemStruct = parse_str( 207 | r#" 208 | #[pyo3::pyclass(mapping, module = "my_module", name = "Placeholder")] 209 | pub struct PyPlaceholder { 210 | #[pyo3(get)] 211 | pub name: String, 212 | } 213 | "#, 214 | )?; 215 | // `#[pyclass]` part 216 | let attrs = parse_pyo3_attr(&item.attrs[0])?; 217 | assert_eq!( 218 | attrs, 219 | vec![ 220 | Attr::Module("my_module".to_string()), 221 | Attr::Name("Placeholder".to_string()) 222 | ] 223 | ); 224 | 225 | // `#[pyo3(get)]` part 226 | if let Fields::Named(fields) = item.fields { 227 | let attrs = parse_pyo3_attr(&fields.named[0].attrs[0])?; 228 | assert_eq!(attrs, vec![Attr::Get]); 229 | } else { 230 | unreachable!() 231 | } 232 | Ok(()) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/member.rs: -------------------------------------------------------------------------------- 1 | use crate::gen_stub::extract_documents; 2 | 3 | use super::{escape_return_type, parse_pyo3_attrs, Attr}; 4 | 5 | use proc_macro2::TokenStream as TokenStream2; 6 | use quote::{quote, ToTokens, TokenStreamExt}; 7 | use syn::{Error, Field, ImplItemFn, Result, Type}; 8 | 9 | #[derive(Debug)] 10 | pub struct MemberInfo { 11 | doc: String, 12 | name: String, 13 | r#type: Type, 14 | } 15 | 16 | impl MemberInfo { 17 | pub fn is_candidate_item(item: &ImplItemFn) -> Result { 18 | let attrs = parse_pyo3_attrs(&item.attrs)?; 19 | Ok(attrs.iter().any(|attr| matches!(attr, Attr::Getter(_)))) 20 | } 21 | 22 | pub fn is_candidate_field(field: &Field) -> Result { 23 | let Field { attrs, .. } = field; 24 | Ok(parse_pyo3_attrs(attrs)? 25 | .iter() 26 | .any(|attr| matches!(attr, Attr::Get))) 27 | } 28 | } 29 | 30 | impl TryFrom for MemberInfo { 31 | type Error = Error; 32 | fn try_from(item: ImplItemFn) -> Result { 33 | assert!(Self::is_candidate_item(&item)?); 34 | let ImplItemFn { attrs, sig, .. } = &item; 35 | let doc = extract_documents(attrs).join("\n"); 36 | let attrs = parse_pyo3_attrs(attrs)?; 37 | for attr in attrs { 38 | if let Attr::Getter(name) = attr { 39 | return Ok(MemberInfo { 40 | doc, 41 | name: name.unwrap_or(sig.ident.to_string()), 42 | r#type: escape_return_type(&sig.output).expect("Getter must return a type"), 43 | }); 44 | } 45 | } 46 | unreachable!("Not a getter: {:?}", item) 47 | } 48 | } 49 | 50 | impl TryFrom for MemberInfo { 51 | type Error = Error; 52 | fn try_from(field: Field) -> Result { 53 | let Field { 54 | ident, ty, attrs, .. 55 | } = field; 56 | let mut field_name = None; 57 | for attr in parse_pyo3_attrs(&attrs)? { 58 | if let Attr::Name(name) = attr { 59 | field_name = Some(name); 60 | } 61 | } 62 | let doc = extract_documents(&attrs).join("\n"); 63 | Ok(Self { 64 | name: field_name.unwrap_or(ident.unwrap().to_string()), 65 | r#type: ty, 66 | doc, 67 | }) 68 | } 69 | } 70 | 71 | impl ToTokens for MemberInfo { 72 | fn to_tokens(&self, tokens: &mut TokenStream2) { 73 | let Self { 74 | name, 75 | r#type: ty, 76 | doc, 77 | } = self; 78 | let name = name.strip_prefix("get_").unwrap_or(name); 79 | tokens.append_all(quote! { 80 | ::pyo3_stub_gen::type_info::MemberInfo { 81 | name: #name, 82 | r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_output, 83 | doc: #doc, 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/method.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | arg::parse_args, escape_return_type, extract_documents, parse_pyo3_attrs, ArgInfo, 3 | ArgsWithSignature, Attr, Signature, 4 | }; 5 | 6 | use proc_macro2::TokenStream as TokenStream2; 7 | use quote::{quote, ToTokens, TokenStreamExt}; 8 | use syn::{ 9 | Error, GenericArgument, ImplItemFn, PathArguments, Result, Type, TypePath, TypeReference, 10 | }; 11 | 12 | #[derive(Debug, Clone, Copy, PartialEq)] 13 | pub enum MethodType { 14 | Instance, 15 | Static, 16 | Class, 17 | New, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct MethodInfo { 22 | name: String, 23 | args: Vec, 24 | sig: Option, 25 | r#return: Option, 26 | doc: String, 27 | r#type: MethodType, 28 | } 29 | 30 | fn replace_inner(ty: &mut Type, self_: &Type) { 31 | match ty { 32 | Type::Path(TypePath { path, .. }) => { 33 | if let Some(last) = path.segments.last_mut() { 34 | if let PathArguments::AngleBracketed(arg) = &mut last.arguments { 35 | for arg in &mut arg.args { 36 | if let GenericArgument::Type(ty) = arg { 37 | replace_inner(ty, self_); 38 | } 39 | } 40 | } 41 | if last.ident == "Self" { 42 | *ty = self_.clone(); 43 | } 44 | } 45 | } 46 | Type::Reference(TypeReference { elem, .. }) => { 47 | replace_inner(elem, self_); 48 | } 49 | _ => {} 50 | } 51 | } 52 | 53 | impl MethodInfo { 54 | pub fn replace_self(&mut self, self_: &Type) { 55 | for arg in &mut self.args { 56 | replace_inner(&mut arg.r#type, self_); 57 | } 58 | if let Some(ret) = self.r#return.as_mut() { 59 | replace_inner(ret, self_); 60 | } 61 | } 62 | } 63 | 64 | impl TryFrom for MethodInfo { 65 | type Error = Error; 66 | fn try_from(item: ImplItemFn) -> Result { 67 | let ImplItemFn { attrs, sig, .. } = item; 68 | let doc = extract_documents(&attrs).join("\n"); 69 | let attrs = parse_pyo3_attrs(&attrs)?; 70 | let mut method_name = None; 71 | let mut text_sig = Signature::overriding_operator(&sig); 72 | let mut method_type = MethodType::Instance; 73 | for attr in attrs { 74 | match attr { 75 | Attr::Name(name) => method_name = Some(name), 76 | Attr::Signature(text_sig_) => text_sig = Some(text_sig_), 77 | Attr::StaticMethod => method_type = MethodType::Static, 78 | Attr::ClassMethod => method_type = MethodType::Class, 79 | Attr::New => method_type = MethodType::New, 80 | _ => {} 81 | } 82 | } 83 | let name = if method_type == MethodType::New { 84 | "__new__".to_string() 85 | } else { 86 | method_name.unwrap_or(sig.ident.to_string()) 87 | }; 88 | let r#return = escape_return_type(&sig.output); 89 | Ok(MethodInfo { 90 | name, 91 | sig: text_sig, 92 | args: parse_args(sig.inputs)?, 93 | r#return, 94 | doc, 95 | r#type: method_type, 96 | }) 97 | } 98 | } 99 | 100 | impl ToTokens for MethodInfo { 101 | fn to_tokens(&self, tokens: &mut TokenStream2) { 102 | let Self { 103 | name, 104 | r#return: ret, 105 | args, 106 | sig, 107 | doc, 108 | r#type, 109 | } = self; 110 | let args_with_sig = ArgsWithSignature { args, sig }; 111 | let ret_tt = if let Some(ret) = ret { 112 | quote! { <#ret as pyo3_stub_gen::PyStubType>::type_output } 113 | } else { 114 | quote! { ::pyo3_stub_gen::type_info::no_return_type_output } 115 | }; 116 | let type_tt = match r#type { 117 | MethodType::Instance => quote! { ::pyo3_stub_gen::type_info::MethodType::Instance }, 118 | MethodType::Static => quote! { ::pyo3_stub_gen::type_info::MethodType::Static }, 119 | MethodType::Class => quote! { ::pyo3_stub_gen::type_info::MethodType::Class }, 120 | MethodType::New => quote! { ::pyo3_stub_gen::type_info::MethodType::New }, 121 | }; 122 | tokens.append_all(quote! { 123 | ::pyo3_stub_gen::type_info::MethodInfo { 124 | name: #name, 125 | args: #args_with_sig, 126 | r#return: #ret_tt, 127 | doc: #doc, 128 | r#type: #type_tt 129 | } 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/pyclass.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{quote, ToTokens, TokenStreamExt}; 3 | use syn::{parse_quote, Error, ItemStruct, Result, Type}; 4 | 5 | use super::{extract_documents, parse_pyo3_attrs, util::quote_option, Attr, MemberInfo, StubType}; 6 | 7 | pub struct PyClassInfo { 8 | pyclass_name: String, 9 | struct_type: Type, 10 | module: Option, 11 | members: Vec, 12 | doc: String, 13 | bases: Vec, 14 | } 15 | 16 | impl From<&PyClassInfo> for StubType { 17 | fn from(info: &PyClassInfo) -> Self { 18 | let PyClassInfo { 19 | pyclass_name, 20 | module, 21 | struct_type, 22 | .. 23 | } = info; 24 | Self { 25 | ty: struct_type.clone(), 26 | name: pyclass_name.clone(), 27 | module: module.clone(), 28 | } 29 | } 30 | } 31 | 32 | impl TryFrom for PyClassInfo { 33 | type Error = Error; 34 | fn try_from(item: ItemStruct) -> Result { 35 | let ItemStruct { 36 | ident, 37 | attrs, 38 | fields, 39 | .. 40 | } = item; 41 | let struct_type: Type = parse_quote!(#ident); 42 | let mut pyclass_name = None; 43 | let mut module = None; 44 | let mut is_get_all = false; 45 | let mut bases = Vec::new(); 46 | for attr in parse_pyo3_attrs(&attrs)? { 47 | match attr { 48 | Attr::Name(name) => pyclass_name = Some(name), 49 | Attr::Module(name) => { 50 | module = Some(name); 51 | } 52 | Attr::GetAll => is_get_all = true, 53 | Attr::Extends(typ) => bases.push(typ), 54 | _ => {} 55 | } 56 | } 57 | let pyclass_name = pyclass_name.unwrap_or_else(|| ident.to_string()); 58 | let mut members = Vec::new(); 59 | for field in fields { 60 | if is_get_all || MemberInfo::is_candidate_field(&field)? { 61 | members.push(MemberInfo::try_from(field)?) 62 | } 63 | } 64 | let doc = extract_documents(&attrs).join("\n"); 65 | Ok(Self { 66 | struct_type, 67 | pyclass_name, 68 | members, 69 | module, 70 | doc, 71 | bases, 72 | }) 73 | } 74 | } 75 | 76 | impl ToTokens for PyClassInfo { 77 | fn to_tokens(&self, tokens: &mut TokenStream2) { 78 | let Self { 79 | pyclass_name, 80 | struct_type, 81 | members, 82 | doc, 83 | module, 84 | bases, 85 | } = self; 86 | let module = quote_option(module); 87 | tokens.append_all(quote! { 88 | ::pyo3_stub_gen::type_info::PyClassInfo { 89 | pyclass_name: #pyclass_name, 90 | struct_id: std::any::TypeId::of::<#struct_type>, 91 | members: &[ #( #members),* ], 92 | module: #module, 93 | doc: #doc, 94 | bases: &[ #( <#bases as ::pyo3_stub_gen::PyStubType>::type_output ),* ], 95 | } 96 | }) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod test { 102 | use super::*; 103 | use syn::parse_str; 104 | 105 | #[test] 106 | fn test_pyclass() -> Result<()> { 107 | let input: ItemStruct = parse_str( 108 | r#" 109 | #[pyclass(mapping, module = "my_module", name = "Placeholder")] 110 | #[derive( 111 | Debug, Clone, PyNeg, PyAdd, PySub, PyMul, PyDiv, PyMod, PyPow, PyCmp, PyIndex, PyPrint, 112 | )] 113 | pub struct PyPlaceholder { 114 | #[pyo3(get)] 115 | pub name: String, 116 | #[pyo3(get)] 117 | pub ndim: usize, 118 | #[pyo3(get)] 119 | pub description: Option, 120 | pub custom_latex: Option, 121 | } 122 | "#, 123 | )?; 124 | let out = PyClassInfo::try_from(input)?.to_token_stream(); 125 | insta::assert_snapshot!(format_as_value(out), @r###" 126 | ::pyo3_stub_gen::type_info::PyClassInfo { 127 | pyclass_name: "Placeholder", 128 | struct_id: std::any::TypeId::of::, 129 | members: &[ 130 | ::pyo3_stub_gen::type_info::MemberInfo { 131 | name: "name", 132 | r#type: ::type_output, 133 | doc: "", 134 | }, 135 | ::pyo3_stub_gen::type_info::MemberInfo { 136 | name: "ndim", 137 | r#type: ::type_output, 138 | doc: "", 139 | }, 140 | ::pyo3_stub_gen::type_info::MemberInfo { 141 | name: "description", 142 | r#type: as ::pyo3_stub_gen::PyStubType>::type_output, 143 | doc: "", 144 | }, 145 | ], 146 | module: Some("my_module"), 147 | doc: "", 148 | bases: &[], 149 | } 150 | "###); 151 | Ok(()) 152 | } 153 | 154 | fn format_as_value(tt: TokenStream2) -> String { 155 | let ttt = quote! { const _: () = #tt; }; 156 | let formatted = prettyplease::unparse(&syn::parse_file(&ttt.to_string()).unwrap()); 157 | formatted 158 | .trim() 159 | .strip_prefix("const _: () = ") 160 | .unwrap() 161 | .strip_suffix(';') 162 | .unwrap() 163 | .to_string() 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/pyclass_enum.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{quote, ToTokens, TokenStreamExt}; 3 | use syn::{parse_quote, Error, ItemEnum, Result, Type}; 4 | 5 | use super::{extract_documents, parse_pyo3_attrs, util::quote_option, Attr, StubType}; 6 | 7 | pub struct PyEnumInfo { 8 | pyclass_name: String, 9 | enum_type: Type, 10 | module: Option, 11 | variants: Vec<(String, String)>, 12 | doc: String, 13 | } 14 | 15 | impl From<&PyEnumInfo> for StubType { 16 | fn from(info: &PyEnumInfo) -> Self { 17 | let PyEnumInfo { 18 | pyclass_name, 19 | module, 20 | enum_type, 21 | .. 22 | } = info; 23 | Self { 24 | ty: enum_type.clone(), 25 | name: pyclass_name.clone(), 26 | module: module.clone(), 27 | } 28 | } 29 | } 30 | 31 | impl TryFrom for PyEnumInfo { 32 | type Error = Error; 33 | fn try_from( 34 | ItemEnum { 35 | variants, 36 | attrs, 37 | ident, 38 | .. 39 | }: ItemEnum, 40 | ) -> Result { 41 | let doc = extract_documents(&attrs).join("\n"); 42 | let mut pyclass_name = None; 43 | let mut module = None; 44 | let mut renaming_rule = None; 45 | for attr in parse_pyo3_attrs(&attrs)? { 46 | match attr { 47 | Attr::Name(name) => pyclass_name = Some(name), 48 | Attr::Module(name) => module = Some(name), 49 | Attr::RenameAll(name) => renaming_rule = Some(name), 50 | _ => {} 51 | } 52 | } 53 | let struct_type = parse_quote!(#ident); 54 | let pyclass_name = pyclass_name.unwrap_or_else(|| ident.to_string()); 55 | let variants = variants 56 | .into_iter() 57 | .map(|var| -> Result<(String, String)> { 58 | let mut var_name = None; 59 | for attr in parse_pyo3_attrs(&var.attrs)? { 60 | if let Attr::Name(name) = attr { 61 | var_name = Some(name); 62 | } 63 | } 64 | let mut var_name = var_name.unwrap_or_else(|| var.ident.to_string()); 65 | if let Some(renaming_rule) = renaming_rule { 66 | var_name = renaming_rule.apply(&var_name); 67 | } 68 | let var_doc = extract_documents(&var.attrs).join("\n"); 69 | Ok((var_name, var_doc)) 70 | }) 71 | .collect::>>()?; 72 | Ok(Self { 73 | doc, 74 | enum_type: struct_type, 75 | pyclass_name, 76 | module, 77 | variants, 78 | }) 79 | } 80 | } 81 | 82 | impl ToTokens for PyEnumInfo { 83 | fn to_tokens(&self, tokens: &mut TokenStream2) { 84 | let Self { 85 | pyclass_name, 86 | enum_type, 87 | variants, 88 | doc, 89 | module, 90 | } = self; 91 | let module = quote_option(module); 92 | let variants: Vec<_> = variants 93 | .iter() 94 | .map(|(name, doc)| quote! {(#name,#doc)}) 95 | .collect(); 96 | tokens.append_all(quote! { 97 | ::pyo3_stub_gen::type_info::PyEnumInfo { 98 | pyclass_name: #pyclass_name, 99 | enum_id: std::any::TypeId::of::<#enum_type>, 100 | variants: &[ #(#variants),* ], 101 | module: #module, 102 | doc: #doc, 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/pyfunction.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{quote, ToTokens, TokenStreamExt}; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | Error, ItemFn, Result, Type, 6 | }; 7 | 8 | use super::{ 9 | escape_return_type, extract_documents, parse_args, parse_pyo3_attrs, quote_option, ArgInfo, 10 | ArgsWithSignature, Attr, Signature, 11 | }; 12 | 13 | pub struct PyFunctionInfo { 14 | name: String, 15 | args: Vec, 16 | r#return: Option, 17 | sig: Option, 18 | doc: String, 19 | module: Option, 20 | } 21 | 22 | struct ModuleAttr { 23 | _module: syn::Ident, 24 | _eq_token: syn::token::Eq, 25 | name: syn::LitStr, 26 | } 27 | 28 | impl Parse for ModuleAttr { 29 | fn parse(input: ParseStream) -> Result { 30 | Ok(Self { 31 | _module: input.parse()?, 32 | _eq_token: input.parse()?, 33 | name: input.parse()?, 34 | }) 35 | } 36 | } 37 | 38 | impl PyFunctionInfo { 39 | pub fn parse_attr(&mut self, attr: TokenStream2) -> Result<()> { 40 | if attr.is_empty() { 41 | return Ok(()); 42 | } 43 | let attr: ModuleAttr = syn::parse2(attr)?; 44 | self.module = Some(attr.name.value()); 45 | Ok(()) 46 | } 47 | } 48 | 49 | impl TryFrom for PyFunctionInfo { 50 | type Error = Error; 51 | fn try_from(item: ItemFn) -> Result { 52 | let doc = extract_documents(&item.attrs).join("\n"); 53 | let args = parse_args(item.sig.inputs)?; 54 | let r#return = escape_return_type(&item.sig.output); 55 | let mut name = None; 56 | let mut sig = None; 57 | for attr in parse_pyo3_attrs(&item.attrs)? { 58 | match attr { 59 | Attr::Name(function_name) => name = Some(function_name), 60 | Attr::Signature(signature) => sig = Some(signature), 61 | _ => {} 62 | } 63 | } 64 | let name = name.unwrap_or_else(|| item.sig.ident.to_string()); 65 | Ok(Self { 66 | args, 67 | sig, 68 | r#return, 69 | name, 70 | doc, 71 | module: None, 72 | }) 73 | } 74 | } 75 | 76 | impl ToTokens for PyFunctionInfo { 77 | fn to_tokens(&self, tokens: &mut TokenStream2) { 78 | let Self { 79 | args, 80 | r#return: ret, 81 | name, 82 | doc, 83 | sig, 84 | module, 85 | } = self; 86 | let ret_tt = if let Some(ret) = ret { 87 | quote! { <#ret as pyo3_stub_gen::PyStubType>::type_output } 88 | } else { 89 | quote! { ::pyo3_stub_gen::type_info::no_return_type_output } 90 | }; 91 | // let sig_tt = quote_option(sig); 92 | let module_tt = quote_option(module); 93 | let args_with_sig = ArgsWithSignature { args, sig }; 94 | tokens.append_all(quote! { 95 | ::pyo3_stub_gen::type_info::PyFunctionInfo { 96 | name: #name, 97 | args: #args_with_sig, 98 | r#return: #ret_tt, 99 | doc: #doc, 100 | module: #module_tt, 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/pymethods.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{quote, ToTokens, TokenStreamExt}; 3 | use syn::{Error, ImplItem, ItemImpl, Result, Type}; 4 | 5 | use super::{MemberInfo, MethodInfo}; 6 | 7 | pub struct PyMethodsInfo { 8 | struct_id: Type, 9 | getters: Vec, 10 | methods: Vec, 11 | } 12 | 13 | impl TryFrom for PyMethodsInfo { 14 | type Error = Error; 15 | fn try_from(item: ItemImpl) -> Result { 16 | let struct_id = *item.self_ty.clone(); 17 | let mut getters = Vec::new(); 18 | let mut methods = Vec::new(); 19 | 20 | for inner in item.items.into_iter() { 21 | let ImplItem::Fn(item_fn) = inner else { 22 | continue; 23 | }; 24 | if MemberInfo::is_candidate_item(&item_fn)? { 25 | getters.push(MemberInfo::try_from(item_fn)?); 26 | continue; 27 | } 28 | 29 | let mut method = MethodInfo::try_from(item_fn)?; 30 | method.replace_self(&item.self_ty); 31 | methods.push(method); 32 | } 33 | Ok(Self { 34 | struct_id, 35 | getters, 36 | methods, 37 | }) 38 | } 39 | } 40 | 41 | impl ToTokens for PyMethodsInfo { 42 | fn to_tokens(&self, tokens: &mut TokenStream2) { 43 | let Self { 44 | struct_id, 45 | getters, 46 | methods, 47 | } = self; 48 | tokens.append_all(quote! { 49 | ::pyo3_stub_gen::type_info::PyMethodsInfo { 50 | struct_id: std::any::TypeId::of::<#struct_id>, 51 | getters: &[ #(#getters),* ], 52 | methods: &[ #(#methods),* ], 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/renaming.rs: -------------------------------------------------------------------------------- 1 | // The following code is copied from "pyo3-macros-backend". If it would be exported, we could reuse it here! 2 | 3 | /// Available renaming rules 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 5 | pub enum RenamingRule { 6 | CamelCase, 7 | KebabCase, 8 | Lowercase, 9 | PascalCase, 10 | ScreamingKebabCase, 11 | ScreamingSnakeCase, 12 | SnakeCase, 13 | Uppercase, 14 | } 15 | 16 | impl RenamingRule { 17 | pub fn try_new(name: &str) -> Option { 18 | match name { 19 | "camelCase" => Some(RenamingRule::CamelCase), 20 | "kebab-case" => Some(RenamingRule::KebabCase), 21 | "lowercase" => Some(RenamingRule::Lowercase), 22 | "PascalCase" => Some(RenamingRule::PascalCase), 23 | "SCREAMING-KEBAB-CASE" => Some(RenamingRule::ScreamingKebabCase), 24 | "SCREAMING_SNAKE_CASE" => Some(RenamingRule::ScreamingSnakeCase), 25 | "snake_case" => Some(RenamingRule::SnakeCase), 26 | "UPPERCASE" => Some(RenamingRule::Uppercase), 27 | _ => None, 28 | } 29 | } 30 | } 31 | 32 | impl RenamingRule { 33 | pub fn apply(self, name: &str) -> String { 34 | use heck::*; 35 | 36 | match self { 37 | RenamingRule::CamelCase => name.to_lower_camel_case(), 38 | RenamingRule::KebabCase => name.to_kebab_case(), 39 | RenamingRule::Lowercase => name.to_lowercase(), 40 | RenamingRule::PascalCase => name.to_upper_camel_case(), 41 | RenamingRule::ScreamingKebabCase => name.to_shouty_kebab_case(), 42 | RenamingRule::ScreamingSnakeCase => name.to_shouty_snake_case(), 43 | RenamingRule::SnakeCase => name.to_snake_case(), 44 | RenamingRule::Uppercase => name.to_uppercase(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/signature.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use proc_macro2::TokenStream as TokenStream2; 4 | use quote::{quote, ToTokens, TokenStreamExt}; 5 | use syn::{ 6 | parenthesized, 7 | parse::{Parse, ParseStream}, 8 | punctuated::Punctuated, 9 | token, Expr, Ident, Result, Token, Type, 10 | }; 11 | 12 | use crate::gen_stub::remove_lifetime; 13 | 14 | use super::ArgInfo; 15 | 16 | #[derive(Debug, Clone, PartialEq)] 17 | enum SignatureArg { 18 | Ident(Ident), 19 | Assign(Ident, Token![=], Expr), 20 | Star(Token![*]), 21 | Args(Token![*], Ident), 22 | Keywords(Token![*], Token![*], Ident), 23 | } 24 | 25 | impl Parse for SignatureArg { 26 | fn parse(input: ParseStream) -> Result { 27 | if input.peek(Token![*]) { 28 | let star = input.parse()?; 29 | if input.peek(Token![*]) { 30 | Ok(SignatureArg::Keywords(star, input.parse()?, input.parse()?)) 31 | } else if input.peek(Ident) { 32 | Ok(SignatureArg::Args(star, input.parse()?)) 33 | } else { 34 | Ok(SignatureArg::Star(star)) 35 | } 36 | } else if input.peek(Ident) { 37 | let ident = Ident::parse(input)?; 38 | if input.peek(Token![=]) { 39 | Ok(SignatureArg::Assign(ident, input.parse()?, input.parse()?)) 40 | } else { 41 | Ok(SignatureArg::Ident(ident)) 42 | } 43 | } else { 44 | dbg!(input); 45 | todo!() 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug, Clone, PartialEq)] 51 | pub struct Signature { 52 | paren: token::Paren, 53 | args: Punctuated, 54 | } 55 | 56 | impl Parse for Signature { 57 | fn parse(input: ParseStream) -> Result { 58 | let content; 59 | let paren = parenthesized!(content in input); 60 | let args = content.parse_terminated(SignatureArg::parse, Token![,])?; 61 | Ok(Self { paren, args }) 62 | } 63 | } 64 | 65 | pub struct ArgsWithSignature<'a> { 66 | pub args: &'a Vec, 67 | pub sig: &'a Option, 68 | } 69 | 70 | impl ToTokens for ArgsWithSignature<'_> { 71 | fn to_tokens(&self, tokens: &mut TokenStream2) { 72 | let arg_infos: Vec = if let Some(sig) = self.sig { 73 | // record all Type information from rust's args 74 | let args_map: HashMap = self 75 | .args 76 | .iter() 77 | .map(|arg| { 78 | let mut ty = arg.r#type.clone(); 79 | remove_lifetime(&mut ty); 80 | (arg.name.clone(), ty) 81 | }) 82 | .collect(); 83 | sig.args.iter().map(|sig_arg| match sig_arg { 84 | SignatureArg::Ident(ident) => { 85 | let name = ident.to_string(); 86 | let ty = args_map.get(&name).unwrap(); 87 | quote! { 88 | ::pyo3_stub_gen::type_info::ArgInfo { 89 | name: #name, 90 | r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_input, 91 | signature: Some(pyo3_stub_gen::type_info::SignatureArg::Ident), 92 | } 93 | } 94 | } 95 | SignatureArg::Assign(ident, _eq, value) => { 96 | let name = ident.to_string(); 97 | let ty = args_map.get(&name).unwrap(); 98 | let default = if value.to_token_stream().to_string() == "None" { 99 | quote! { 100 | "None".to_string() 101 | } 102 | } else { 103 | quote! { 104 | ::pyo3::prepare_freethreaded_python(); 105 | ::pyo3::Python::with_gil(|py| -> String { 106 | let v: #ty = #value; 107 | ::pyo3_stub_gen::util::fmt_py_obj(py, v) 108 | }) 109 | } 110 | }; 111 | quote! { 112 | ::pyo3_stub_gen::type_info::ArgInfo { 113 | name: #name, 114 | r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_input, 115 | signature: Some(pyo3_stub_gen::type_info::SignatureArg::Assign{ 116 | default: { 117 | static DEFAULT: std::sync::LazyLock = std::sync::LazyLock::new(|| { 118 | #default 119 | }); 120 | &DEFAULT 121 | } 122 | }), 123 | } 124 | } 125 | }, 126 | SignatureArg::Star(_) => quote! { 127 | ::pyo3_stub_gen::type_info::ArgInfo { 128 | name: "", 129 | r#type: <() as ::pyo3_stub_gen::PyStubType>::type_input, 130 | signature: Some(pyo3_stub_gen::type_info::SignatureArg::Star), 131 | } 132 | }, 133 | SignatureArg::Args(_, ident) => { 134 | let name = ident.to_string(); 135 | let ty = args_map.get(&name).unwrap(); 136 | quote! { 137 | ::pyo3_stub_gen::type_info::ArgInfo { 138 | name: #name, 139 | r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_input, 140 | signature: Some(pyo3_stub_gen::type_info::SignatureArg::Args), 141 | } 142 | } 143 | }, 144 | SignatureArg::Keywords(_, _, ident) => { 145 | let name = ident.to_string(); 146 | let ty = args_map.get(&name).unwrap(); 147 | quote! { 148 | ::pyo3_stub_gen::type_info::ArgInfo { 149 | name: #name, 150 | r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_input, 151 | signature: Some(pyo3_stub_gen::type_info::SignatureArg::Keywords), 152 | } 153 | } 154 | } 155 | }).collect() 156 | } else { 157 | self.args 158 | .iter() 159 | .map(|arg| { 160 | let mut ty = arg.r#type.clone(); 161 | remove_lifetime(&mut ty); 162 | let name = &arg.name; 163 | quote! { 164 | ::pyo3_stub_gen::type_info::ArgInfo { 165 | name: #name, 166 | r#type: <#ty as ::pyo3_stub_gen::PyStubType>::type_input, 167 | signature: None, 168 | } 169 | } 170 | }) 171 | .collect() 172 | }; 173 | tokens.append_all(quote! { &[ #(#arg_infos),* ] }); 174 | } 175 | } 176 | 177 | impl Signature { 178 | pub fn overriding_operator(sig: &syn::Signature) -> Option { 179 | if sig.ident == "__pow__" { 180 | return Some(syn::parse_str("(exponent, modulo=None)").unwrap()); 181 | } 182 | if sig.ident == "__rpow__" { 183 | return Some(syn::parse_str("(base, modulo=None)").unwrap()); 184 | } 185 | None 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/stub_type.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{quote, ToTokens, TokenStreamExt}; 3 | use syn::Type; 4 | 5 | pub struct StubType { 6 | pub(crate) ty: Type, 7 | pub(crate) name: String, 8 | pub(crate) module: Option, 9 | } 10 | 11 | impl ToTokens for StubType { 12 | fn to_tokens(&self, tokens: &mut TokenStream2) { 13 | let Self { ty, name, module } = self; 14 | let module_tt = if let Some(module) = module { 15 | quote! { #module.into() } 16 | } else { 17 | quote! { Default::default() } 18 | }; 19 | 20 | tokens.append_all(quote! { 21 | #[automatically_derived] 22 | impl ::pyo3_stub_gen::PyStubType for #ty { 23 | fn type_output() -> ::pyo3_stub_gen::TypeInfo { 24 | ::pyo3_stub_gen::TypeInfo::with_module(#name, #module_tt) 25 | } 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/gen_stub/util.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::{quote, ToTokens}; 3 | use syn::{GenericArgument, PathArguments, PathSegment, ReturnType, Type, TypePath}; 4 | 5 | pub fn quote_option(a: &Option) -> TokenStream2 { 6 | if let Some(a) = a { 7 | quote! { Some(#a) } 8 | } else { 9 | quote! { None } 10 | } 11 | } 12 | 13 | pub fn remove_lifetime(ty: &mut Type) { 14 | match ty { 15 | Type::Path(TypePath { path, .. }) => { 16 | if let Some(PathSegment { 17 | arguments: PathArguments::AngleBracketed(inner), 18 | .. 19 | }) = path.segments.last_mut() 20 | { 21 | for arg in &mut inner.args { 22 | match arg { 23 | GenericArgument::Lifetime(l) => { 24 | // `T::<'a, S>` becomes `T::<'_, S>` 25 | *l = syn::parse_quote!('_); 26 | } 27 | GenericArgument::Type(ty) => { 28 | remove_lifetime(ty); 29 | } 30 | _ => {} 31 | } 32 | } 33 | } 34 | } 35 | Type::Reference(rty) => { 36 | rty.lifetime = None; 37 | remove_lifetime(rty.elem.as_mut()); 38 | } 39 | Type::Tuple(ty) => { 40 | for elem in &mut ty.elems { 41 | remove_lifetime(elem); 42 | } 43 | } 44 | Type::Array(ary) => { 45 | remove_lifetime(ary.elem.as_mut()); 46 | } 47 | _ => {} 48 | } 49 | } 50 | 51 | /// Extract `T` from `PyResult`. 52 | /// 53 | /// For `PyResult<&'a T>` case, `'a` will be removed, i.e. returns `&T` for this case. 54 | pub fn escape_return_type(ret: &ReturnType) -> Option { 55 | let ret = if let ReturnType::Type(_, ty) = ret { 56 | unwrap_pyresult(ty) 57 | } else { 58 | return None; 59 | }; 60 | let mut ret = ret.clone(); 61 | remove_lifetime(&mut ret); 62 | Some(ret) 63 | } 64 | 65 | fn unwrap_pyresult(ty: &Type) -> &Type { 66 | if let Type::Path(TypePath { path, .. }) = ty { 67 | if let Some(last) = path.segments.last() { 68 | if last.ident == "PyResult" { 69 | if let PathArguments::AngleBracketed(inner) = &last.arguments { 70 | for arg in &inner.args { 71 | if let GenericArgument::Type(ty) = arg { 72 | return ty; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | ty 80 | } 81 | 82 | #[cfg(test)] 83 | mod test { 84 | use super::*; 85 | use syn::{parse_str, Result}; 86 | 87 | #[test] 88 | fn test_unwrap_pyresult() -> Result<()> { 89 | let ty: Type = parse_str("PyResult")?; 90 | let out = unwrap_pyresult(&ty); 91 | assert_eq!(out, &parse_str("i32")?); 92 | 93 | let ty: Type = parse_str("PyResult<&PyString>")?; 94 | let out = unwrap_pyresult(&ty); 95 | assert_eq!(out, &parse_str("&PyString")?); 96 | 97 | let ty: Type = parse_str("PyResult<&'a PyString>")?; 98 | let out = unwrap_pyresult(&ty); 99 | assert_eq!(out, &parse_str("&'a PyString")?); 100 | 101 | let ty: Type = parse_str("::pyo3::PyResult")?; 102 | let out = unwrap_pyresult(&ty); 103 | assert_eq!(out, &parse_str("i32")?); 104 | 105 | let ty: Type = parse_str("::pyo3::PyResult<&PyString>")?; 106 | let out = unwrap_pyresult(&ty); 107 | assert_eq!(out, &parse_str("&PyString")?); 108 | 109 | let ty: Type = parse_str("::pyo3::PyResult<&'a PyString>")?; 110 | let out = unwrap_pyresult(&ty); 111 | assert_eq!(out, &parse_str("&'a PyString")?); 112 | 113 | Ok(()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pyo3-stub-gen-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod gen_stub; 2 | 3 | use proc_macro::TokenStream; 4 | 5 | /// Embed metadata for Python stub file generation for `#[pyclass]` macro 6 | /// 7 | /// ``` 8 | /// #[pyo3_stub_gen_derive::gen_stub_pyclass] 9 | /// #[pyo3::pyclass(mapping, module = "my_module", name = "Placeholder")] 10 | /// #[derive(Debug, Clone)] 11 | /// pub struct PyPlaceholder { 12 | /// #[pyo3(get)] 13 | /// pub name: String, 14 | /// #[pyo3(get)] 15 | /// pub ndim: usize, 16 | /// #[pyo3(get)] 17 | /// pub description: Option, 18 | /// pub custom_latex: Option, 19 | /// } 20 | /// ``` 21 | #[proc_macro_attribute] 22 | pub fn gen_stub_pyclass(_attr: TokenStream, item: TokenStream) -> TokenStream { 23 | gen_stub::pyclass(item.into()) 24 | .unwrap_or_else(|err| err.to_compile_error()) 25 | .into() 26 | } 27 | 28 | /// Embed metadata for Python stub file generation for `#[pyclass]` macro with enum 29 | /// 30 | /// ``` 31 | /// #[pyo3_stub_gen_derive::gen_stub_pyclass_enum] 32 | /// #[pyo3::pyclass(module = "my_module", name = "DataType")] 33 | /// #[derive(Debug, Clone, PartialEq, Eq, Hash)] 34 | /// pub enum PyDataType { 35 | /// #[pyo3(name = "FLOAT")] 36 | /// Float, 37 | /// #[pyo3(name = "INTEGER")] 38 | /// Integer, 39 | /// } 40 | /// ``` 41 | #[proc_macro_attribute] 42 | pub fn gen_stub_pyclass_enum(_attr: TokenStream, item: TokenStream) -> TokenStream { 43 | gen_stub::pyclass_enum(item.into()) 44 | .unwrap_or_else(|err| err.to_compile_error()) 45 | .into() 46 | } 47 | 48 | /// Embed metadata for Python stub file generation for `#[pymethods]` macro 49 | /// 50 | /// ``` 51 | /// #[pyo3_stub_gen_derive::gen_stub_pyclass] 52 | /// #[pyo3::pyclass] 53 | /// struct A {} 54 | /// 55 | /// #[pyo3_stub_gen_derive::gen_stub_pymethods] 56 | /// #[pyo3::pymethods] 57 | /// impl A { 58 | /// #[getter] 59 | /// fn f(&self) -> Vec { 60 | /// todo!() 61 | /// } 62 | /// } 63 | /// ``` 64 | #[proc_macro_attribute] 65 | pub fn gen_stub_pymethods(_attr: TokenStream, item: TokenStream) -> TokenStream { 66 | gen_stub::pymethods(item.into()) 67 | .unwrap_or_else(|err| err.to_compile_error()) 68 | .into() 69 | } 70 | 71 | /// Embed metadata for Python stub file generation for `#[pyfunction]` macro 72 | /// 73 | /// ``` 74 | /// #[pyo3_stub_gen_derive::gen_stub_pyfunction] 75 | /// #[pyo3::pyfunction] 76 | /// #[pyo3(name = "is_odd")] 77 | /// pub fn is_odd(x: u32) -> bool { 78 | /// todo!() 79 | /// } 80 | /// ``` 81 | /// 82 | /// The function attributed by `#[gen_stub_pyfunction]` will be appended to default stub file. 83 | /// If you want to append this function to another module, add `module` attribute. 84 | /// 85 | /// ``` 86 | /// #[pyo3_stub_gen_derive::gen_stub_pyfunction(module = "my_module.experimental")] 87 | /// #[pyo3::pyfunction] 88 | /// #[pyo3(name = "is_odd")] 89 | /// pub fn is_odd(x: u32) -> bool { 90 | /// todo!() 91 | /// } 92 | /// ``` 93 | #[proc_macro_attribute] 94 | pub fn gen_stub_pyfunction(attr: TokenStream, item: TokenStream) -> TokenStream { 95 | gen_stub::pyfunction(attr.into(), item.into()) 96 | .unwrap_or_else(|err| err.to_compile_error()) 97 | .into() 98 | } 99 | -------------------------------------------------------------------------------- /pyo3-stub-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyo3-stub-gen" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | keywords.workspace = true 8 | license.workspace = true 9 | readme.workspace = true 10 | 11 | [dependencies] 12 | anyhow.workspace = true 13 | chrono.workspace = true 14 | indexmap.workspace = true 15 | inventory.workspace = true 16 | itertools.workspace = true 17 | log.workspace = true 18 | maplit.workspace = true 19 | num-complex.workspace = true 20 | numpy = { workspace = true, optional = true } 21 | either = { workspace = true, optional = true } 22 | pyo3.workspace = true 23 | serde.workspace = true 24 | toml.workspace = true 25 | 26 | [dependencies.pyo3-stub-gen-derive] 27 | version = "0.9.1" 28 | path = "../pyo3-stub-gen-derive" 29 | 30 | [dev-dependencies] 31 | test-case.workspace = true 32 | 33 | [features] 34 | default = ["numpy", "either"] 35 | numpy = ["dep:numpy"] 36 | either = ["dep:either"] 37 | 38 | [build-dependencies] 39 | pyo3-build-config = { version = "0.24", features = ["resolve-config"] } 40 | cargo_metadata = "0.19" 41 | semver = "1.0" 42 | 43 | [lints.rust] 44 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(pyo3_0_25)'] } 45 | -------------------------------------------------------------------------------- /pyo3-stub-gen/build.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | 3 | fn main() { 4 | // Add all pyo3's #[cfg] flags to the current compilation. 5 | pyo3_build_config::use_pyo3_cfgs(); 6 | 7 | // Add `pyo3_0_25` flag if pyo3's version >= 0.25.0 8 | let metadata = cargo_metadata::MetadataCommand::new() 9 | .features(cargo_metadata::CargoOpt::AllFeatures) 10 | .exec() 11 | .expect("Failed to run cargo metadata"); 12 | let pyo3_pkg = metadata 13 | .packages 14 | .iter() 15 | .find(|p| p.name == "pyo3") 16 | .expect("`pyo3` not found in dependencies"); 17 | let pyo3_ver = Version::parse(&pyo3_pkg.version.to_string()).expect("Invalid semver for pyo3"); 18 | if pyo3_ver >= Version::new(0, 25, 0) { 19 | println!("cargo::rustc-check-cfg=cfg(pyo3_0_25)"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/exception.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::*; 2 | 3 | #[macro_export] 4 | macro_rules! create_exception { 5 | ($module: expr, $name: ident, $base: ty) => { 6 | $crate::create_exception!($module, $name, $base, ""); 7 | }; 8 | ($module: expr, $name: ident, $base: ty, $doc: expr) => { 9 | ::pyo3::create_exception!($module, $name, $base, $doc); 10 | 11 | $crate::inventory::submit! { 12 | $crate::type_info::PyErrorInfo { 13 | name: stringify!($name), 14 | module: stringify!($module), 15 | base: <$base as $crate::exception::NativeException>::type_name, 16 | } 17 | } 18 | }; 19 | } 20 | 21 | /// Native exceptions in Python 22 | pub trait NativeException { 23 | /// Type name in Python side 24 | fn type_name() -> &'static str; 25 | } 26 | 27 | macro_rules! impl_native_exception { 28 | ($name:ident, $type_name:literal) => { 29 | impl NativeException for $name { 30 | fn type_name() -> &'static str { 31 | $type_name 32 | } 33 | } 34 | }; 35 | } 36 | 37 | impl_native_exception!(PyArithmeticError, "ArithmeticError"); 38 | impl_native_exception!(PyAssertionError, "AssertionError"); 39 | impl_native_exception!(PyAttributeError, "AttributeError"); 40 | impl_native_exception!(PyBaseException, "BaseException"); 41 | impl_native_exception!(PyBlockingIOError, "BlockingIOError"); 42 | impl_native_exception!(PyBrokenPipeError, "BrokenPipeError"); 43 | impl_native_exception!(PyBufferError, "BufferError"); 44 | impl_native_exception!(PyBytesWarning, "BytesWarning"); 45 | impl_native_exception!(PyChildProcessError, "ChildProcessError"); 46 | impl_native_exception!(PyConnectionAbortedError, "ConnectionAbortedError"); 47 | impl_native_exception!(PyConnectionError, "ConnectionError"); 48 | impl_native_exception!(PyConnectionRefusedError, "ConnectionRefusedError"); 49 | impl_native_exception!(PyConnectionResetError, "ConnectionResetError"); 50 | impl_native_exception!(PyDeprecationWarning, "DeprecationWarning"); 51 | impl_native_exception!(PyEOFError, "EOFError"); 52 | // FIXME: This only exists in Python 3.10+. 53 | // We need to find a way to conditionally compile this. 54 | // impl_native_exception!(PyEncodingWarning, "EncodingWarning"); 55 | impl_native_exception!(PyEnvironmentError, "EnvironmentError"); 56 | impl_native_exception!(PyException, "Exception"); 57 | impl_native_exception!(PyFileExistsError, "FileExistsError"); 58 | impl_native_exception!(PyFileNotFoundError, "FileNotFoundError"); 59 | impl_native_exception!(PyFloatingPointError, "FloatingPointError"); 60 | impl_native_exception!(PyFutureWarning, "FutureWarning"); 61 | impl_native_exception!(PyGeneratorExit, "GeneratorExit"); 62 | impl_native_exception!(PyIOError, "IOError"); 63 | impl_native_exception!(PyImportError, "ImportError"); 64 | impl_native_exception!(PyImportWarning, "ImportWarning"); 65 | impl_native_exception!(PyIndexError, "IndexError"); 66 | impl_native_exception!(PyInterruptedError, "InterruptedError"); 67 | impl_native_exception!(PyIsADirectoryError, "IsADirectoryError"); 68 | impl_native_exception!(PyKeyError, "KeyError"); 69 | impl_native_exception!(PyKeyboardInterrupt, "KeyboardInterrupt"); 70 | impl_native_exception!(PyLookupError, "LookupError"); 71 | impl_native_exception!(PyMemoryError, "MemoryError"); 72 | impl_native_exception!(PyModuleNotFoundError, "ModuleNotFoundError"); 73 | impl_native_exception!(PyNameError, "NameError"); 74 | impl_native_exception!(PyNotADirectoryError, "NotADirectoryError"); 75 | impl_native_exception!(PyNotImplementedError, "NotImplementedError"); 76 | impl_native_exception!(PyOSError, "OSError"); 77 | impl_native_exception!(PyOverflowError, "OverflowError"); 78 | impl_native_exception!(PyPendingDeprecationWarning, "PendingDeprecationWarning"); 79 | impl_native_exception!(PyPermissionError, "PermissionError"); 80 | impl_native_exception!(PyProcessLookupError, "ProcessLookupError"); 81 | impl_native_exception!(PyRecursionError, "RecursionError"); 82 | impl_native_exception!(PyReferenceError, "ReferenceError"); 83 | impl_native_exception!(PyResourceWarning, "ResourceWarning"); 84 | impl_native_exception!(PyRuntimeError, "RuntimeError"); 85 | impl_native_exception!(PyRuntimeWarning, "RuntimeWarning"); 86 | impl_native_exception!(PyStopAsyncIteration, "StopAsyncIteration"); 87 | impl_native_exception!(PyStopIteration, "StopIteration"); 88 | impl_native_exception!(PySyntaxError, "SyntaxError"); 89 | impl_native_exception!(PySyntaxWarning, "SyntaxWarning"); 90 | impl_native_exception!(PySystemError, "SystemError"); 91 | impl_native_exception!(PySystemExit, "SystemExit"); 92 | impl_native_exception!(PyTimeoutError, "TimeoutError"); 93 | impl_native_exception!(PyTypeError, "TypeError"); 94 | impl_native_exception!(PyUnboundLocalError, "UnboundLocalError"); 95 | impl_native_exception!(PyUnicodeDecodeError, "UnicodeDecodeError"); 96 | impl_native_exception!(PyUnicodeEncodeError, "UnicodeEncodeError"); 97 | impl_native_exception!(PyUnicodeError, "UnicodeError"); 98 | impl_native_exception!(PyUnicodeTranslateError, "UnicodeTranslateError"); 99 | impl_native_exception!(PyUnicodeWarning, "UnicodeWarning"); 100 | impl_native_exception!(PyUserWarning, "UserWarning"); 101 | impl_native_exception!(PyValueError, "ValueError"); 102 | impl_native_exception!(PyWarning, "Warning"); 103 | impl_native_exception!(PyZeroDivisionError, "ZeroDivisionError"); 104 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate.rs: -------------------------------------------------------------------------------- 1 | //! Generate Python typing stub file a.k.a. `*.pyi` file. 2 | 3 | mod arg; 4 | mod class; 5 | mod docstring; 6 | mod enum_; 7 | mod error; 8 | mod function; 9 | mod member; 10 | mod method; 11 | mod module; 12 | mod stub_info; 13 | mod variable; 14 | 15 | pub use arg::*; 16 | pub use class::*; 17 | pub use enum_::*; 18 | pub use error::*; 19 | pub use function::*; 20 | pub use member::*; 21 | pub use method::*; 22 | pub use module::*; 23 | pub use stub_info::*; 24 | pub use variable::*; 25 | 26 | use crate::stub_type::ModuleRef; 27 | use std::collections::HashSet; 28 | 29 | fn indent() -> &'static str { 30 | " " 31 | } 32 | 33 | pub trait Import { 34 | fn import(&self) -> HashSet; 35 | } 36 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/arg.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::Import, stub_type::ModuleRef, type_info::*, TypeInfo}; 2 | use std::{collections::HashSet, fmt}; 3 | 4 | #[derive(Debug, Clone, PartialEq)] 5 | pub struct Arg { 6 | pub name: &'static str, 7 | pub r#type: TypeInfo, 8 | pub signature: Option, 9 | } 10 | 11 | impl Import for Arg { 12 | fn import(&self) -> HashSet { 13 | self.r#type.import.clone() 14 | } 15 | } 16 | 17 | impl From<&ArgInfo> for Arg { 18 | fn from(info: &ArgInfo) -> Self { 19 | Self { 20 | name: info.name, 21 | r#type: (info.r#type)(), 22 | signature: info.signature.clone(), 23 | } 24 | } 25 | } 26 | 27 | impl fmt::Display for Arg { 28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 29 | if let Some(signature) = &self.signature { 30 | match signature { 31 | SignatureArg::Ident => write!(f, "{}:{}", self.name, self.r#type), 32 | SignatureArg::Assign { default } => { 33 | let default: &String = default; 34 | write!(f, "{}:{}={}", self.name, self.r#type, default) 35 | } 36 | SignatureArg::Star => write!(f, "*"), 37 | SignatureArg::Args => write!(f, "*{}", self.name), 38 | SignatureArg::Keywords => write!(f, "**{}", self.name), 39 | } 40 | } else { 41 | write!(f, "{}:{}", self.name, self.r#type) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/class.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::*, type_info::*, TypeInfo}; 2 | use std::fmt; 3 | 4 | /// Definition of a Python class. 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct ClassDef { 7 | pub name: &'static str, 8 | pub doc: &'static str, 9 | pub members: Vec, 10 | pub methods: Vec, 11 | pub bases: Vec, 12 | } 13 | 14 | impl Import for ClassDef { 15 | fn import(&self) -> HashSet { 16 | let mut import = HashSet::new(); 17 | for base in &self.bases { 18 | import.extend(base.import.clone()); 19 | } 20 | for member in &self.members { 21 | import.extend(member.import()); 22 | } 23 | for method in &self.methods { 24 | import.extend(method.import()); 25 | } 26 | import 27 | } 28 | } 29 | 30 | impl From<&PyClassInfo> for ClassDef { 31 | fn from(info: &PyClassInfo) -> Self { 32 | // Since there are multiple `#[pymethods]` for a single class, we need to merge them. 33 | // This is only an initializer. See `StubInfo::gather` for the actual merging. 34 | Self { 35 | name: info.pyclass_name, 36 | doc: info.doc, 37 | members: info.members.iter().map(MemberDef::from).collect(), 38 | methods: Vec::new(), 39 | bases: info.bases.iter().map(|f| f()).collect(), 40 | } 41 | } 42 | } 43 | 44 | impl fmt::Display for ClassDef { 45 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 46 | let bases = self 47 | .bases 48 | .iter() 49 | .map(|i| i.name.clone()) 50 | .reduce(|acc, path| format!("{acc}, {path}")) 51 | .map(|bases| format!("({bases})")) 52 | .unwrap_or_default(); 53 | writeln!(f, "class {}{}:", self.name, bases)?; 54 | let indent = indent(); 55 | let doc = self.doc.trim(); 56 | docstring::write_docstring(f, doc, indent)?; 57 | for member in &self.members { 58 | member.fmt(f)?; 59 | } 60 | for method in &self.methods { 61 | method.fmt(f)?; 62 | } 63 | if self.members.is_empty() && self.methods.is_empty() { 64 | writeln!(f, "{indent}...")?; 65 | } 66 | writeln!(f)?; 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/docstring.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub fn write_docstring(f: &mut fmt::Formatter, doc: &str, indent: &str) -> fmt::Result { 4 | let doc = doc.trim(); 5 | if !doc.is_empty() { 6 | writeln!(f, r#"{indent}r""""#)?; 7 | for line in doc.lines() { 8 | writeln!(f, "{indent}{line}")?; 9 | } 10 | writeln!(f, r#"{indent}""""#)?; 11 | } 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/enum_.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::*, type_info::*}; 2 | use std::fmt; 3 | 4 | /// Definition of a Python enum. 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct EnumDef { 7 | pub name: &'static str, 8 | pub doc: &'static str, 9 | pub variants: &'static [(&'static str, &'static str)], 10 | pub methods: Vec, 11 | pub members: Vec, 12 | } 13 | 14 | impl From<&PyEnumInfo> for EnumDef { 15 | fn from(info: &PyEnumInfo) -> Self { 16 | Self { 17 | name: info.pyclass_name, 18 | doc: info.doc, 19 | variants: info.variants, 20 | methods: Vec::new(), 21 | members: Vec::new(), 22 | } 23 | } 24 | } 25 | 26 | impl fmt::Display for EnumDef { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | writeln!(f, "class {}(Enum):", self.name)?; 29 | let indent = indent(); 30 | docstring::write_docstring(f, self.doc, indent)?; 31 | for (variant, variant_doc) in self.variants { 32 | writeln!(f, "{indent}{} = ...", variant)?; 33 | docstring::write_docstring(f, variant_doc, indent)?; 34 | } 35 | for member in &self.members { 36 | writeln!(f)?; 37 | member.fmt(f)?; 38 | } 39 | for methods in &self.methods { 40 | writeln!(f)?; 41 | methods.fmt(f)?; 42 | } 43 | writeln!(f)?; 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/error.rs: -------------------------------------------------------------------------------- 1 | use crate::type_info::*; 2 | use std::fmt; 3 | 4 | /// Definition of a Python execption. 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct ErrorDef { 7 | pub name: &'static str, 8 | pub base: &'static str, 9 | } 10 | 11 | impl From<&PyErrorInfo> for ErrorDef { 12 | fn from(info: &PyErrorInfo) -> Self { 13 | Self { 14 | name: info.name, 15 | base: (info.base)(), 16 | } 17 | } 18 | } 19 | 20 | impl fmt::Display for ErrorDef { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | writeln!(f, "class {}({}): ...", self.name, self.base) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/function.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::*, type_info::*, TypeInfo}; 2 | use std::fmt; 3 | 4 | /// Definition of a Python function. 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct FunctionDef { 7 | pub name: &'static str, 8 | pub args: Vec, 9 | pub r#return: TypeInfo, 10 | pub doc: &'static str, 11 | } 12 | 13 | impl Import for FunctionDef { 14 | fn import(&self) -> HashSet { 15 | let mut import = self.r#return.import.clone(); 16 | for arg in &self.args { 17 | import.extend(arg.import().into_iter()); 18 | } 19 | import 20 | } 21 | } 22 | 23 | impl From<&PyFunctionInfo> for FunctionDef { 24 | fn from(info: &PyFunctionInfo) -> Self { 25 | Self { 26 | name: info.name, 27 | args: info.args.iter().map(Arg::from).collect(), 28 | r#return: (info.r#return)(), 29 | doc: info.doc, 30 | } 31 | } 32 | } 33 | 34 | impl fmt::Display for FunctionDef { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | write!(f, "def {}(", self.name)?; 37 | for (i, arg) in self.args.iter().enumerate() { 38 | write!(f, "{}", arg)?; 39 | if i != self.args.len() - 1 { 40 | write!(f, ", ")?; 41 | } 42 | } 43 | write!(f, ") -> {}:", self.r#return)?; 44 | 45 | let doc = self.doc; 46 | if !doc.is_empty() { 47 | writeln!(f)?; 48 | docstring::write_docstring(f, self.doc, indent())?; 49 | } else { 50 | writeln!(f, " ...")?; 51 | } 52 | writeln!(f)?; 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/member.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::*, type_info::*, TypeInfo}; 2 | use std::{ 3 | collections::HashSet, 4 | fmt::{self}, 5 | }; 6 | 7 | /// Definition of a class member. 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct MemberDef { 10 | pub name: &'static str, 11 | pub r#type: TypeInfo, 12 | pub doc: &'static str, 13 | } 14 | 15 | impl Import for MemberDef { 16 | fn import(&self) -> HashSet { 17 | self.r#type.import.clone() 18 | } 19 | } 20 | 21 | impl From<&MemberInfo> for MemberDef { 22 | fn from(info: &MemberInfo) -> Self { 23 | Self { 24 | name: info.name, 25 | r#type: (info.r#type)(), 26 | doc: info.doc, 27 | } 28 | } 29 | } 30 | 31 | impl fmt::Display for MemberDef { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | let indent = indent(); 34 | writeln!(f, "{indent}{}: {}", self.name, self.r#type)?; 35 | docstring::write_docstring(f, self.doc, indent)?; 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/method.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::*, type_info::*, TypeInfo}; 2 | use std::{collections::HashSet, fmt}; 3 | 4 | pub use crate::type_info::MethodType; 5 | 6 | /// Definition of a class method. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct MethodDef { 9 | pub name: &'static str, 10 | pub args: Vec, 11 | pub r#return: TypeInfo, 12 | pub doc: &'static str, 13 | pub r#type: MethodType, 14 | } 15 | 16 | impl Import for MethodDef { 17 | fn import(&self) -> HashSet { 18 | let mut import = self.r#return.import.clone(); 19 | for arg in &self.args { 20 | import.extend(arg.import().into_iter()); 21 | } 22 | import 23 | } 24 | } 25 | 26 | impl From<&MethodInfo> for MethodDef { 27 | fn from(info: &MethodInfo) -> Self { 28 | Self { 29 | name: info.name, 30 | args: info.args.iter().map(Arg::from).collect(), 31 | r#return: (info.r#return)(), 32 | doc: info.doc, 33 | r#type: info.r#type, 34 | } 35 | } 36 | } 37 | 38 | impl fmt::Display for MethodDef { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | let indent = indent(); 41 | let mut needs_comma = false; 42 | match self.r#type { 43 | MethodType::Static => { 44 | writeln!(f, "{indent}@staticmethod")?; 45 | write!(f, "{indent}def {}(", self.name)?; 46 | } 47 | MethodType::Class | MethodType::New => { 48 | if self.r#type == MethodType::Class { 49 | // new is a classmethod without the decorator 50 | writeln!(f, "{indent}@classmethod")?; 51 | } 52 | write!(f, "{indent}def {}(cls", self.name)?; 53 | needs_comma = true; 54 | } 55 | MethodType::Instance => { 56 | write!(f, "{indent}def {}(self", self.name)?; 57 | needs_comma = true; 58 | } 59 | } 60 | for arg in &self.args { 61 | if needs_comma { 62 | write!(f, ", ")?; 63 | } 64 | write!(f, "{}", arg)?; 65 | needs_comma = true; 66 | } 67 | write!(f, ") -> {}:", self.r#return)?; 68 | 69 | let doc = self.doc; 70 | if !doc.is_empty() { 71 | writeln!(f)?; 72 | let double_indent = format!("{indent}{indent}"); 73 | docstring::write_docstring(f, self.doc, &double_indent)?; 74 | } else { 75 | writeln!(f, " ...")?; 76 | } 77 | Ok(()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/module.rs: -------------------------------------------------------------------------------- 1 | use crate::generate::*; 2 | use itertools::Itertools; 3 | use std::{ 4 | any::TypeId, 5 | collections::{BTreeMap, BTreeSet}, 6 | fmt, 7 | }; 8 | 9 | /// Type info for a Python (sub-)module. This corresponds to a single `*.pyi` file. 10 | #[derive(Debug, Clone, PartialEq, Default)] 11 | pub struct Module { 12 | pub class: BTreeMap, 13 | pub enum_: BTreeMap, 14 | pub function: BTreeMap<&'static str, FunctionDef>, 15 | pub error: BTreeMap<&'static str, ErrorDef>, 16 | pub variables: BTreeMap<&'static str, VariableDef>, 17 | pub name: String, 18 | pub default_module_name: String, 19 | /// Direct submodules of this module. 20 | pub submodules: BTreeSet, 21 | } 22 | 23 | impl Import for Module { 24 | fn import(&self) -> HashSet { 25 | let mut imports = HashSet::new(); 26 | for class in self.class.values() { 27 | imports.extend(class.import()); 28 | } 29 | for function in self.function.values() { 30 | imports.extend(function.import()); 31 | } 32 | imports 33 | } 34 | } 35 | 36 | impl fmt::Display for Module { 37 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 38 | writeln!(f, "# This file is automatically generated by pyo3_stub_gen")?; 39 | writeln!(f, "# ruff: noqa: E501, F401")?; 40 | writeln!(f)?; 41 | for import in self.import().into_iter().sorted() { 42 | let name = import.get().unwrap_or(&self.default_module_name); 43 | if name != self.name { 44 | writeln!(f, "import {}", name)?; 45 | } 46 | } 47 | for submod in &self.submodules { 48 | writeln!(f, "from . import {}", submod)?; 49 | } 50 | if !self.enum_.is_empty() { 51 | writeln!(f, "from enum import Enum")?; 52 | } 53 | writeln!(f)?; 54 | 55 | for var in self.variables.values() { 56 | writeln!(f, "{}", var)?; 57 | } 58 | for class in self.class.values().sorted_by_key(|class| class.name) { 59 | write!(f, "{}", class)?; 60 | } 61 | for enum_ in self.enum_.values().sorted_by_key(|class| class.name) { 62 | write!(f, "{}", enum_)?; 63 | } 64 | for function in self.function.values() { 65 | write!(f, "{}", function)?; 66 | } 67 | for error in self.error.values() { 68 | writeln!(f, "{}", error)?; 69 | } 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/stub_info.rs: -------------------------------------------------------------------------------- 1 | use crate::{generate::*, pyproject::PyProject, type_info::*}; 2 | use anyhow::{Context, Result}; 3 | use std::{ 4 | collections::{BTreeMap, BTreeSet}, 5 | fs, 6 | io::Write, 7 | path::*, 8 | }; 9 | 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub struct StubInfo { 12 | pub modules: BTreeMap, 13 | pub python_root: PathBuf, 14 | } 15 | 16 | impl StubInfo { 17 | /// Initialize [StubInfo] from a `pyproject.toml` file in `CARGO_MANIFEST_DIR`. 18 | /// This is automatically set up by the [crate::define_stub_info_gatherer] macro. 19 | pub fn from_pyproject_toml(path: impl AsRef) -> Result { 20 | let pyproject = PyProject::parse_toml(path)?; 21 | Ok(StubInfoBuilder::from_pyproject_toml(pyproject).build()) 22 | } 23 | 24 | /// Initialize [StubInfo] with a specific module name and project root. 25 | /// This must be placed in your PyO3 library crate, i.e. the same crate where [inventory::submit]ted, 26 | /// not in the `gen_stub` executables due to [inventory]'s mechanism. 27 | pub fn from_project_root(default_module_name: String, project_root: PathBuf) -> Result { 28 | Ok(StubInfoBuilder::from_project_root(default_module_name, project_root).build()) 29 | } 30 | 31 | pub fn generate(&self) -> Result<()> { 32 | for (name, module) in self.modules.iter() { 33 | let path = name.replace(".", "/"); 34 | let dest = if module.submodules.is_empty() { 35 | self.python_root.join(format!("{path}.pyi")) 36 | } else { 37 | self.python_root.join(path).join("__init__.pyi") 38 | }; 39 | 40 | let dir = dest.parent().context("Cannot get parent directory")?; 41 | if !dir.exists() { 42 | fs::create_dir_all(dir)?; 43 | } 44 | 45 | let mut f = fs::File::create(&dest)?; 46 | write!(f, "{}", module)?; 47 | log::info!( 48 | "Generate stub file of a module `{name}` at {dest}", 49 | dest = dest.display() 50 | ); 51 | } 52 | Ok(()) 53 | } 54 | } 55 | 56 | struct StubInfoBuilder { 57 | modules: BTreeMap, 58 | default_module_name: String, 59 | python_root: PathBuf, 60 | } 61 | 62 | impl StubInfoBuilder { 63 | fn from_pyproject_toml(pyproject: PyProject) -> Self { 64 | StubInfoBuilder::from_project_root( 65 | pyproject.module_name().to_string(), 66 | pyproject 67 | .python_source() 68 | .unwrap_or(PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())), 69 | ) 70 | } 71 | 72 | fn from_project_root(default_module_name: String, project_root: PathBuf) -> Self { 73 | Self { 74 | modules: BTreeMap::new(), 75 | default_module_name, 76 | python_root: project_root, 77 | } 78 | } 79 | 80 | fn get_module(&mut self, name: Option<&str>) -> &mut Module { 81 | let name = name.unwrap_or(&self.default_module_name).to_string(); 82 | let module = self.modules.entry(name.clone()).or_default(); 83 | module.name = name; 84 | module.default_module_name = self.default_module_name.clone(); 85 | module 86 | } 87 | 88 | fn register_submodules(&mut self) { 89 | let mut map: BTreeMap> = BTreeMap::new(); 90 | for module in self.modules.keys() { 91 | let path = module.split('.').collect::>(); 92 | let n = path.len(); 93 | if n <= 1 { 94 | continue; 95 | } 96 | map.entry(path[..n - 1].join(".")) 97 | .or_default() 98 | .insert(path[n - 1].to_string()); 99 | } 100 | for (parent, children) in map { 101 | if let Some(module) = self.modules.get_mut(&parent) { 102 | module.submodules.extend(children); 103 | } 104 | } 105 | } 106 | 107 | fn add_class(&mut self, info: &PyClassInfo) { 108 | self.get_module(info.module) 109 | .class 110 | .insert((info.struct_id)(), ClassDef::from(info)); 111 | } 112 | 113 | fn add_enum(&mut self, info: &PyEnumInfo) { 114 | self.get_module(info.module) 115 | .enum_ 116 | .insert((info.enum_id)(), EnumDef::from(info)); 117 | } 118 | 119 | fn add_function(&mut self, info: &PyFunctionInfo) { 120 | self.get_module(info.module) 121 | .function 122 | .insert(info.name, FunctionDef::from(info)); 123 | } 124 | 125 | fn add_error(&mut self, info: &PyErrorInfo) { 126 | self.get_module(Some(info.module)) 127 | .error 128 | .insert(info.name, ErrorDef::from(info)); 129 | } 130 | 131 | fn add_variable(&mut self, info: &PyVariableInfo) { 132 | self.get_module(Some(info.module)) 133 | .variables 134 | .insert(info.name, VariableDef::from(info)); 135 | } 136 | 137 | fn add_methods(&mut self, info: &PyMethodsInfo) { 138 | let struct_id = (info.struct_id)(); 139 | for module in self.modules.values_mut() { 140 | if let Some(entry) = module.class.get_mut(&struct_id) { 141 | for getter in info.getters { 142 | entry.members.push(MemberDef { 143 | name: getter.name, 144 | r#type: (getter.r#type)(), 145 | doc: getter.doc, 146 | }); 147 | } 148 | for method in info.methods { 149 | entry.methods.push(MethodDef::from(method)) 150 | } 151 | return; 152 | } else if let Some(entry) = module.enum_.get_mut(&struct_id) { 153 | for getter in info.getters { 154 | entry.members.push(MemberDef { 155 | name: getter.name, 156 | r#type: (getter.r#type)(), 157 | doc: getter.doc, 158 | }); 159 | } 160 | for method in info.methods { 161 | entry.methods.push(MethodDef::from(method)) 162 | } 163 | return; 164 | } 165 | } 166 | unreachable!("Missing struct_id/enum_id = {:?}", struct_id); 167 | } 168 | 169 | fn build(mut self) -> StubInfo { 170 | for info in inventory::iter:: { 171 | self.add_class(info); 172 | } 173 | for info in inventory::iter:: { 174 | self.add_enum(info); 175 | } 176 | for info in inventory::iter:: { 177 | self.add_function(info); 178 | } 179 | for info in inventory::iter:: { 180 | self.add_error(info); 181 | } 182 | for info in inventory::iter:: { 183 | self.add_variable(info); 184 | } 185 | for info in inventory::iter:: { 186 | self.add_methods(info); 187 | } 188 | self.register_submodules(); 189 | StubInfo { 190 | modules: self.modules, 191 | python_root: self.python_root, 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/generate/variable.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{type_info::PyVariableInfo, TypeInfo}; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct VariableDef { 7 | pub name: &'static str, 8 | pub type_: TypeInfo, 9 | } 10 | 11 | impl From<&PyVariableInfo> for VariableDef { 12 | fn from(info: &PyVariableInfo) -> Self { 13 | Self { 14 | name: info.name, 15 | type_: (info.r#type)(), 16 | } 17 | } 18 | } 19 | 20 | impl fmt::Display for VariableDef { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | write!(f, "{}: {}", self.name, self.type_) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate creates stub files in following three steps using [inventory] crate: 2 | //! 3 | //! Define type information in Rust code (or by proc-macro) 4 | //! --------------------------------------------------------- 5 | //! The first step is to define Python type information in Rust code. [type_info] module provides several structs, for example: 6 | //! 7 | //! - [type_info::PyFunctionInfo] stores information of Python function, i.e. the name of the function, arguments and its types, return type, etc. 8 | //! - [type_info::PyClassInfo] stores information for Python class definition, i.e. the name of the class, members and its types, methods, etc. 9 | //! 10 | //! For better understanding of what happens in the background, let's define these information manually: 11 | //! 12 | //! ``` 13 | //! use pyo3::*; 14 | //! use pyo3_stub_gen::type_info::*; 15 | //! 16 | //! // Usual PyO3 class definition 17 | //! #[pyclass(module = "my_module", name = "MyClass")] 18 | //! struct MyClass { 19 | //! #[pyo3(get)] 20 | //! name: String, 21 | //! #[pyo3(get)] 22 | //! description: Option, 23 | //! } 24 | //! 25 | //! // Submit type information for stub file generation to inventory 26 | //! inventory::submit!{ 27 | //! // Send information about Python class 28 | //! PyClassInfo { 29 | //! // Type ID of Rust struct (used to gathering phase discussed later) 30 | //! struct_id: std::any::TypeId::of::, 31 | //! 32 | //! // Python module name. Since stub file is generated per modules, 33 | //! // this helps where the class definition should be placed. 34 | //! module: Some("my_module"), 35 | //! 36 | //! // Python class name 37 | //! pyclass_name: "MyClass", 38 | //! 39 | //! members: &[ 40 | //! MemberInfo { 41 | //! name: "name", 42 | //! r#type: ::type_output, 43 | //! doc: "Name docstring", 44 | //! }, 45 | //! MemberInfo { 46 | //! name: "description", 47 | //! r#type: as ::pyo3_stub_gen::PyStubType>::type_output, 48 | //! doc: "Description docstring", 49 | //! }, 50 | //! ], 51 | //! 52 | //! doc: "Docstring used in Python", 53 | //! 54 | //! // Base classes 55 | //! bases: &[], 56 | //! } 57 | //! } 58 | //! ``` 59 | //! 60 | //! Roughly speaking, the above corresponds a following stub file `my_module.pyi`: 61 | //! 62 | //! ```python 63 | //! class MyClass: 64 | //! """ 65 | //! Docstring used in Python 66 | //! """ 67 | //! name: str 68 | //! """Name docstring""" 69 | //! description: Optional[str] 70 | //! """Description docstring""" 71 | //! ``` 72 | //! 73 | //! We want to generate this [type_info::PyClassInfo] section automatically from `MyClass` Rust struct definition. 74 | //! This is done by using `#[gen_stub_pyclass]` proc-macro: 75 | //! 76 | //! ``` 77 | //! use pyo3::*; 78 | //! use pyo3_stub_gen::{type_info::*, derive::gen_stub_pyclass}; 79 | //! 80 | //! // Usual PyO3 class definition 81 | //! #[gen_stub_pyclass] 82 | //! #[pyclass(module = "my_module", name = "MyClass")] 83 | //! struct MyClass { 84 | //! #[pyo3(get)] 85 | //! name: String, 86 | //! #[pyo3(get)] 87 | //! description: Option, 88 | //! } 89 | //! ``` 90 | //! 91 | //! Since proc-macro is a converter from Rust code to Rust code, the output must be a Rust code. 92 | //! However, we need to gather these [type_info::PyClassInfo] definitions to generate stub files, 93 | //! and the above [inventory::submit] is for it. 94 | //! 95 | //! Gather type information into [StubInfo] 96 | //! ---------------------------------------- 97 | //! [inventory] crate provides a mechanism to gather [inventory::submit]ted information when the library is loaded. 98 | //! To access these information through [inventory::iter], we need to define a gather function in the crate. 99 | //! Typically, this is done by following: 100 | //! 101 | //! ```rust 102 | //! use pyo3_stub_gen::{StubInfo, Result}; 103 | //! 104 | //! pub fn stub_info() -> Result { 105 | //! let manifest_dir: &::std::path::Path = env!("CARGO_MANIFEST_DIR").as_ref(); 106 | //! StubInfo::from_pyproject_toml(manifest_dir.join("pyproject.toml")) 107 | //! } 108 | //! ``` 109 | //! 110 | //! There is a helper macro to define it easily: 111 | //! 112 | //! ```rust 113 | //! pyo3_stub_gen::define_stub_info_gatherer!(sub_info); 114 | //! ``` 115 | //! 116 | //! Generate stub file from [StubInfo] 117 | //! ----------------------------------- 118 | //! [StubInfo] translates [type_info::PyClassInfo] and other information into a form helpful for generating stub files while gathering. 119 | //! 120 | //! [generate] module provides structs implementing [std::fmt::Display] to generate corresponding parts of stub file. 121 | //! For example, [generate::MethodDef] generates Python class method definition as follows: 122 | //! 123 | //! ```rust 124 | //! use pyo3_stub_gen::{TypeInfo, generate::*}; 125 | //! 126 | //! let method = MethodDef { 127 | //! name: "foo", 128 | //! args: vec![Arg { name: "x", r#type: TypeInfo::builtin("int"), signature: None, }], 129 | //! r#return: TypeInfo::builtin("int"), 130 | //! doc: "This is a foo method.", 131 | //! r#type: MethodType::Instance, 132 | //! }; 133 | //! 134 | //! assert_eq!( 135 | //! method.to_string().trim(), 136 | //! r#" 137 | //! def foo(self, x:builtins.int) -> builtins.int: 138 | //! r""" 139 | //! This is a foo method. 140 | //! """ 141 | //! "#.trim() 142 | //! ); 143 | //! ``` 144 | //! 145 | //! [generate::ClassDef] generates Python class definition using [generate::MethodDef] and others, and other `*Def` structs works as well. 146 | //! 147 | //! [generate::Module] consists of `*Def` structs and yields an entire stub file `*.pyi` for a single Python (sub-)module, i.e. a shared library build by PyO3. 148 | //! [generate::Module]s are created as a part of [StubInfo], which merges [type_info::PyClassInfo]s and others submitted to [inventory] separately. 149 | //! [StubInfo] is instantiated with [pyproject::PyProject] to get where to generate the stub file, 150 | //! and [StubInfo::generate] generates the stub files for every modules. 151 | //! 152 | 153 | pub use inventory; 154 | pub use pyo3_stub_gen_derive as derive; // re-export to use in generated code 155 | 156 | pub mod exception; 157 | pub mod generate; 158 | pub mod pyproject; 159 | mod stub_type; 160 | pub mod type_info; 161 | pub mod util; 162 | 163 | pub use generate::StubInfo; 164 | pub use stub_type::{PyStubType, TypeInfo}; 165 | 166 | pub type Result = anyhow::Result; 167 | 168 | /// Create a function to initialize [StubInfo] from `pyproject.toml` in `CARGO_MANIFEST_DIR`. 169 | /// 170 | /// If `pyproject.toml` is in another place, you need to create a function to call [StubInfo::from_pyproject_toml] manually. 171 | /// This must be placed in your PyO3 library crate, i.e. same crate where [inventory::submit]ted, 172 | /// not in `gen_stub` executables due to [inventory] mechanism. 173 | /// 174 | #[macro_export] 175 | macro_rules! define_stub_info_gatherer { 176 | ($function_name:ident) => { 177 | /// Auto-generated function to gather information to generate stub files 178 | pub fn $function_name() -> $crate::Result<$crate::StubInfo> { 179 | let manifest_dir: &::std::path::Path = env!("CARGO_MANIFEST_DIR").as_ref(); 180 | $crate::StubInfo::from_pyproject_toml(manifest_dir.join("pyproject.toml")) 181 | } 182 | }; 183 | } 184 | 185 | #[macro_export] 186 | macro_rules! module_variable { 187 | ($module:expr, $name:expr, $ty:ty) => { 188 | $crate::inventory::submit! { 189 | $crate::type_info::PyVariableInfo{ 190 | name: $name, 191 | module: $module, 192 | r#type: <$ty as $crate::PyStubType>::type_output, 193 | } 194 | } 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/pyproject.rs: -------------------------------------------------------------------------------- 1 | //! `pyproject.toml` parser for reading `[tool.maturin]` configuration. 2 | //! 3 | //! ``` 4 | //! use pyo3_stub_gen::pyproject::PyProject; 5 | //! use std::path::Path; 6 | //! 7 | //! let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); 8 | //! let pyproject = PyProject::parse_toml( 9 | //! root.join("examples/mixed/pyproject.toml") 10 | //! ).unwrap(); 11 | //! ``` 12 | 13 | use anyhow::{bail, Result}; 14 | use serde::{Deserialize, Serialize}; 15 | use std::{fs, path::*}; 16 | 17 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 18 | pub struct PyProject { 19 | pub project: Project, 20 | pub tool: Option, 21 | 22 | #[serde(skip)] 23 | toml_path: PathBuf, 24 | } 25 | 26 | impl PyProject { 27 | pub fn parse_toml(path: impl AsRef) -> Result { 28 | let path = path.as_ref(); 29 | if path.file_name() != Some("pyproject.toml".as_ref()) { 30 | bail!("{} is not a pyproject.toml", path.display()) 31 | } 32 | let mut out: PyProject = toml::de::from_str(&fs::read_to_string(path)?)?; 33 | out.toml_path = path.to_path_buf(); 34 | Ok(out) 35 | } 36 | 37 | pub fn module_name(&self) -> &str { 38 | if let Some(tool) = &self.tool { 39 | if let Some(maturin) = &tool.maturin { 40 | if let Some(module_name) = &maturin.module_name { 41 | return module_name; 42 | } 43 | } 44 | } 45 | &self.project.name 46 | } 47 | 48 | /// Return `tool.maturin.python_source` if it exists, which means the project is a mixed Rust/Python project. 49 | pub fn python_source(&self) -> Option { 50 | if let Some(tool) = &self.tool { 51 | if let Some(maturin) = &tool.maturin { 52 | if let Some(python_source) = &maturin.python_source { 53 | if let Some(base) = self.toml_path.parent() { 54 | return Some(base.join(python_source)); 55 | } else { 56 | return Some(PathBuf::from(python_source)); 57 | } 58 | } 59 | } 60 | } 61 | None 62 | } 63 | } 64 | 65 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 66 | pub struct Project { 67 | pub name: String, 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 71 | pub struct Tool { 72 | pub maturin: Option, 73 | } 74 | 75 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 76 | pub struct Maturin { 77 | #[serde(rename = "python-source")] 78 | pub python_source: Option, 79 | #[serde(rename = "module-name")] 80 | pub module_name: Option, 81 | } 82 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/stub_type.rs: -------------------------------------------------------------------------------- 1 | mod builtins; 2 | mod collections; 3 | mod pyo3; 4 | 5 | #[cfg(feature = "numpy")] 6 | mod numpy; 7 | 8 | #[cfg(feature = "either")] 9 | mod either; 10 | 11 | use maplit::hashset; 12 | use std::{collections::HashSet, fmt, ops}; 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)] 15 | pub enum ModuleRef { 16 | Named(String), 17 | 18 | /// Default module that PyO3 creates. 19 | /// 20 | /// - For pure Rust project, the default module name is the crate name specified in `Cargo.toml` 21 | /// or `project.name` specified in `pyproject.toml` 22 | /// - For mixed Rust/Python project, the default module name is `tool.maturin.module-name` specified in `pyproject.toml` 23 | /// 24 | /// Because the default module name cannot be known at compile time, it will be resolved at the time of the stub file generation. 25 | /// This is a placeholder for the default module name. 26 | #[default] 27 | Default, 28 | } 29 | 30 | impl ModuleRef { 31 | pub fn get(&self) -> Option<&str> { 32 | match self { 33 | Self::Named(name) => Some(name), 34 | Self::Default => None, 35 | } 36 | } 37 | } 38 | 39 | impl From<&str> for ModuleRef { 40 | fn from(s: &str) -> Self { 41 | Self::Named(s.to_string()) 42 | } 43 | } 44 | 45 | /// Type information for creating Python stub files annotated by [PyStubType] trait. 46 | #[derive(Debug, Clone, PartialEq, Eq)] 47 | pub struct TypeInfo { 48 | /// The Python type name. 49 | pub name: String, 50 | 51 | /// Python modules must be imported in the stub file. 52 | /// 53 | /// For example, when `name` is `typing.Sequence[int]`, `import` should contain `typing`. 54 | /// This makes it possible to use user-defined types in the stub file. 55 | pub import: HashSet, 56 | } 57 | 58 | impl fmt::Display for TypeInfo { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | write!(f, "{}", self.name) 61 | } 62 | } 63 | 64 | impl TypeInfo { 65 | /// A `None` type annotation. 66 | pub fn none() -> Self { 67 | // NOTE: since 3.10, NoneType is provided from types module, 68 | // but there is no corresponding definitions prior to 3.10. 69 | Self { 70 | name: "None".to_string(), 71 | import: HashSet::new(), 72 | } 73 | } 74 | 75 | /// A `typing.Any` type annotation. 76 | pub fn any() -> Self { 77 | Self { 78 | name: "typing.Any".to_string(), 79 | import: hashset! { "typing".into() }, 80 | } 81 | } 82 | 83 | /// A `list[Type]` type annotation. 84 | pub fn list_of() -> Self { 85 | let TypeInfo { name, mut import } = T::type_output(); 86 | import.insert("builtins".into()); 87 | TypeInfo { 88 | name: format!("builtins.list[{}]", name), 89 | import, 90 | } 91 | } 92 | 93 | /// A `set[Type]` type annotation. 94 | pub fn set_of() -> Self { 95 | let TypeInfo { name, mut import } = T::type_output(); 96 | import.insert("builtins".into()); 97 | TypeInfo { 98 | name: format!("builtins.set[{}]", name), 99 | import, 100 | } 101 | } 102 | 103 | /// A `dict[Type]` type annotation. 104 | pub fn dict_of() -> Self { 105 | let TypeInfo { 106 | name: name_k, 107 | mut import, 108 | } = K::type_output(); 109 | let TypeInfo { 110 | name: name_v, 111 | import: import_v, 112 | } = V::type_output(); 113 | import.extend(import_v); 114 | import.insert("builtins".into()); 115 | TypeInfo { 116 | name: format!("builtins.set[{}, {}]", name_k, name_v), 117 | import, 118 | } 119 | } 120 | 121 | /// A type annotation of a built-in type provided from `builtins` module, such as `int`, `str`, or `float`. Generic builtin types are also possible, such as `dict[str, str]`. 122 | pub fn builtin(name: &str) -> Self { 123 | Self { 124 | name: format!("builtins.{name}"), 125 | import: hashset! { "builtins".into() }, 126 | } 127 | } 128 | 129 | /// Unqualified type. 130 | pub fn unqualified(name: &str) -> Self { 131 | Self { 132 | name: name.to_string(), 133 | import: hashset! {}, 134 | } 135 | } 136 | 137 | /// A type annotation of a type that must be imported. The type name must be qualified with the module name: 138 | /// 139 | /// ``` 140 | /// pyo3_stub_gen::TypeInfo::with_module("pathlib.Path", "pathlib".into()); 141 | /// ``` 142 | pub fn with_module(name: &str, module: ModuleRef) -> Self { 143 | let mut import = HashSet::new(); 144 | import.insert(module); 145 | Self { 146 | name: name.to_string(), 147 | import, 148 | } 149 | } 150 | } 151 | 152 | impl ops::BitOr for TypeInfo { 153 | type Output = Self; 154 | 155 | fn bitor(mut self, rhs: Self) -> Self { 156 | self.import.extend(rhs.import); 157 | Self { 158 | name: format!("{} | {}", self.name, rhs.name), 159 | import: self.import, 160 | } 161 | } 162 | } 163 | 164 | /// Implement [PyStubType] 165 | /// 166 | /// ```rust 167 | /// use pyo3::*; 168 | /// use pyo3_stub_gen::{impl_stub_type, derive::*}; 169 | /// 170 | /// #[gen_stub_pyclass] 171 | /// #[pyclass] 172 | /// struct A; 173 | /// 174 | /// #[gen_stub_pyclass] 175 | /// #[pyclass] 176 | /// struct B; 177 | /// 178 | /// enum E { 179 | /// A(A), 180 | /// B(B), 181 | /// } 182 | /// impl_stub_type!(E = A | B); 183 | /// 184 | /// struct X(A); 185 | /// impl_stub_type!(X = A); 186 | /// 187 | /// struct Y { 188 | /// a: A, 189 | /// b: B, 190 | /// } 191 | /// impl_stub_type!(Y = (A, B)); 192 | /// ``` 193 | #[macro_export] 194 | macro_rules! impl_stub_type { 195 | ($ty: ty = $($base:ty)|+) => { 196 | impl ::pyo3_stub_gen::PyStubType for $ty { 197 | fn type_output() -> ::pyo3_stub_gen::TypeInfo { 198 | $(<$base>::type_output()) | * 199 | } 200 | fn type_input() -> ::pyo3_stub_gen::TypeInfo { 201 | $(<$base>::type_input()) | * 202 | } 203 | } 204 | }; 205 | ($ty:ty = $base:ty) => { 206 | impl ::pyo3_stub_gen::PyStubType for $ty { 207 | fn type_output() -> ::pyo3_stub_gen::TypeInfo { 208 | <$base>::type_output() 209 | } 210 | fn type_input() -> ::pyo3_stub_gen::TypeInfo { 211 | <$base>::type_input() 212 | } 213 | } 214 | }; 215 | } 216 | 217 | /// Annotate Rust types with Python type information. 218 | pub trait PyStubType { 219 | /// The type to be used in the output signature, i.e. return type of the Python function or methods. 220 | fn type_output() -> TypeInfo; 221 | 222 | /// The type to be used in the input signature, i.e. the arguments of the Python function or methods. 223 | /// 224 | /// This defaults to the output type, but can be overridden for types that are not valid input types. 225 | /// For example, `Vec::::type_output` returns `list[T]` while `Vec::::type_input` returns `typing.Sequence[T]`. 226 | fn type_input() -> TypeInfo { 227 | Self::type_output() 228 | } 229 | } 230 | 231 | #[cfg(test)] 232 | mod test { 233 | use super::*; 234 | use maplit::hashset; 235 | use std::collections::HashMap; 236 | use test_case::test_case; 237 | 238 | #[test_case(bool::type_input(), "builtins.bool", hashset! { "builtins".into() } ; "bool_input")] 239 | #[test_case(<&str>::type_input(), "builtins.str", hashset! { "builtins".into() } ; "str_input")] 240 | #[test_case(Vec::::type_input(), "typing.Sequence[builtins.int]", hashset! { "typing".into(), "builtins".into() } ; "Vec_u32_input")] 241 | #[test_case(Vec::::type_output(), "builtins.list[builtins.int]", hashset! { "builtins".into() } ; "Vec_u32_output")] 242 | #[test_case(HashMap::::type_input(), "typing.Mapping[builtins.int, builtins.str]", hashset! { "typing".into(), "builtins".into() } ; "HashMap_u32_String_input")] 243 | #[test_case(HashMap::::type_output(), "builtins.dict[builtins.int, builtins.str]", hashset! { "builtins".into() } ; "HashMap_u32_String_output")] 244 | #[test_case(indexmap::IndexMap::::type_input(), "typing.Mapping[builtins.int, builtins.str]", hashset! { "typing".into(), "builtins".into() } ; "IndexMap_u32_String_input")] 245 | #[test_case(indexmap::IndexMap::::type_output(), "builtins.dict[builtins.int, builtins.str]", hashset! { "builtins".into() } ; "IndexMap_u32_String_output")] 246 | #[test_case(HashMap::>::type_input(), "typing.Mapping[builtins.int, typing.Sequence[builtins.int]]", hashset! { "builtins".into(), "typing".into() } ; "HashMap_u32_Vec_u32_input")] 247 | #[test_case(HashMap::>::type_output(), "builtins.dict[builtins.int, builtins.list[builtins.int]]", hashset! { "builtins".into() } ; "HashMap_u32_Vec_u32_output")] 248 | #[test_case(HashSet::::type_input(), "builtins.set[builtins.int]", hashset! { "builtins".into() } ; "HashSet_u32_input")] 249 | #[test_case(indexmap::IndexSet::::type_input(), "builtins.set[builtins.int]", hashset! { "builtins".into() } ; "IndexSet_u32_input")] 250 | fn test(tinfo: TypeInfo, name: &str, import: HashSet) { 251 | assert_eq!(tinfo.name, name); 252 | if import.is_empty() { 253 | assert!(tinfo.import.is_empty()); 254 | } else { 255 | assert_eq!(tinfo.import, import); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/stub_type/builtins.rs: -------------------------------------------------------------------------------- 1 | //! Define PyStubType for built-in types based on 2 | 3 | use crate::stub_type::*; 4 | use std::{ 5 | borrow::Cow, 6 | ffi::{OsStr, OsString}, 7 | path::PathBuf, 8 | rc::Rc, 9 | sync::Arc, 10 | time::SystemTime, 11 | }; 12 | 13 | use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; 14 | 15 | macro_rules! impl_builtin { 16 | ($ty:ty, $pytype:expr) => { 17 | impl PyStubType for $ty { 18 | fn type_output() -> TypeInfo { 19 | TypeInfo::builtin($pytype) 20 | } 21 | } 22 | }; 23 | } 24 | 25 | macro_rules! impl_with_module { 26 | ($ty:ty, $pytype:expr, $module:expr) => { 27 | impl PyStubType for $ty { 28 | fn type_output() -> TypeInfo { 29 | TypeInfo::with_module($pytype, $module.into()) 30 | } 31 | } 32 | }; 33 | } 34 | 35 | // NOTE: 36 | impl PyStubType for () { 37 | fn type_output() -> TypeInfo { 38 | TypeInfo::none() 39 | } 40 | } 41 | impl_builtin!(bool, "bool"); 42 | impl_builtin!(u8, "int"); 43 | impl_builtin!(u16, "int"); 44 | impl_builtin!(u32, "int"); 45 | impl_builtin!(u64, "int"); 46 | impl_builtin!(u128, "int"); 47 | impl_builtin!(usize, "int"); 48 | impl_builtin!(i8, "int"); 49 | impl_builtin!(i16, "int"); 50 | impl_builtin!(i32, "int"); 51 | impl_builtin!(i64, "int"); 52 | impl_builtin!(i128, "int"); 53 | impl_builtin!(isize, "int"); 54 | impl_builtin!(f32, "float"); 55 | impl_builtin!(f64, "float"); 56 | impl_builtin!(num_complex::Complex32, "complex"); 57 | impl_builtin!(num_complex::Complex64, "complex"); 58 | 59 | impl_builtin!(char, "str"); 60 | impl_builtin!(&str, "str"); 61 | impl_builtin!(OsStr, "str"); 62 | impl_builtin!(String, "str"); 63 | impl_builtin!(OsString, "str"); 64 | impl_builtin!(Cow<'_, str>, "str"); 65 | impl_builtin!(Cow<'_, OsStr>, "str"); 66 | impl_builtin!(Cow<'_, [u8]>, "bytes"); 67 | 68 | impl PyStubType for PathBuf { 69 | fn type_output() -> TypeInfo { 70 | TypeInfo::with_module("pathlib.Path", "pathlib".into()) 71 | } 72 | fn type_input() -> TypeInfo { 73 | TypeInfo::builtin("str") 74 | | TypeInfo::with_module("os.PathLike", "os".into()) 75 | | TypeInfo::with_module("pathlib.Path", "pathlib".into()) 76 | } 77 | } 78 | 79 | impl PyStubType for DateTime { 80 | fn type_output() -> TypeInfo { 81 | TypeInfo::with_module("datetime.datetime", "datetime".into()) 82 | } 83 | } 84 | 85 | impl_with_module!(SystemTime, "datetime.datetime", "datetime"); 86 | impl_with_module!(NaiveDateTime, "datetime.datetime", "datetime"); 87 | impl_with_module!(NaiveDate, "datetime.date", "datetime"); 88 | impl_with_module!(NaiveTime, "datetime.time", "datetime"); 89 | impl_with_module!(FixedOffset, "datetime.tzinfo", "datetime"); 90 | impl_with_module!(Utc, "datetime.tzinfo", "datetime"); 91 | impl_with_module!(std::time::Duration, "datetime.timedelta", "datetime"); 92 | impl_with_module!(chrono::Duration, "datetime.timedelta", "datetime"); 93 | 94 | impl PyStubType for &T { 95 | fn type_input() -> TypeInfo { 96 | T::type_input() 97 | } 98 | fn type_output() -> TypeInfo { 99 | T::type_output() 100 | } 101 | } 102 | 103 | impl PyStubType for Rc { 104 | fn type_input() -> TypeInfo { 105 | T::type_input() 106 | } 107 | fn type_output() -> TypeInfo { 108 | T::type_output() 109 | } 110 | } 111 | 112 | impl PyStubType for Arc { 113 | fn type_input() -> TypeInfo { 114 | T::type_input() 115 | } 116 | fn type_output() -> TypeInfo { 117 | T::type_output() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/stub_type/collections.rs: -------------------------------------------------------------------------------- 1 | use crate::stub_type::*; 2 | use std::collections::{BTreeMap, BTreeSet, HashMap}; 3 | 4 | impl PyStubType for Option { 5 | fn type_input() -> TypeInfo { 6 | let TypeInfo { name, mut import } = T::type_input(); 7 | import.insert("typing".into()); 8 | TypeInfo { 9 | name: format!("typing.Optional[{}]", name), 10 | import, 11 | } 12 | } 13 | fn type_output() -> TypeInfo { 14 | let TypeInfo { name, mut import } = T::type_output(); 15 | import.insert("typing".into()); 16 | TypeInfo { 17 | name: format!("typing.Optional[{}]", name), 18 | import, 19 | } 20 | } 21 | } 22 | 23 | impl PyStubType for Box { 24 | fn type_input() -> TypeInfo { 25 | T::type_input() 26 | } 27 | fn type_output() -> TypeInfo { 28 | T::type_output() 29 | } 30 | } 31 | 32 | impl PyStubType for Result { 33 | fn type_input() -> TypeInfo { 34 | T::type_input() 35 | } 36 | fn type_output() -> TypeInfo { 37 | T::type_output() 38 | } 39 | } 40 | 41 | impl PyStubType for Vec { 42 | fn type_input() -> TypeInfo { 43 | let TypeInfo { name, mut import } = T::type_input(); 44 | import.insert("typing".into()); 45 | TypeInfo { 46 | name: format!("typing.Sequence[{}]", name), 47 | import, 48 | } 49 | } 50 | fn type_output() -> TypeInfo { 51 | TypeInfo::list_of::() 52 | } 53 | } 54 | 55 | impl PyStubType for [T; N] { 56 | fn type_input() -> TypeInfo { 57 | let TypeInfo { name, mut import } = T::type_input(); 58 | import.insert("typing".into()); 59 | TypeInfo { 60 | name: format!("typing.Sequence[{}]", name), 61 | import, 62 | } 63 | } 64 | fn type_output() -> TypeInfo { 65 | TypeInfo::list_of::() 66 | } 67 | } 68 | 69 | impl PyStubType for HashSet { 70 | fn type_output() -> TypeInfo { 71 | TypeInfo::set_of::() 72 | } 73 | } 74 | 75 | impl PyStubType for BTreeSet { 76 | fn type_output() -> TypeInfo { 77 | TypeInfo::set_of::() 78 | } 79 | } 80 | 81 | impl PyStubType for indexmap::IndexSet { 82 | fn type_output() -> TypeInfo { 83 | TypeInfo::set_of::() 84 | } 85 | } 86 | 87 | macro_rules! impl_map_inner { 88 | () => { 89 | fn type_input() -> TypeInfo { 90 | let TypeInfo { 91 | name: key_name, 92 | mut import, 93 | } = Key::type_input(); 94 | let TypeInfo { 95 | name: value_name, 96 | import: value_import, 97 | } = Value::type_input(); 98 | import.extend(value_import); 99 | import.insert("typing".into()); 100 | TypeInfo { 101 | name: format!("typing.Mapping[{}, {}]", key_name, value_name), 102 | import, 103 | } 104 | } 105 | fn type_output() -> TypeInfo { 106 | let TypeInfo { 107 | name: key_name, 108 | mut import, 109 | } = Key::type_output(); 110 | let TypeInfo { 111 | name: value_name, 112 | import: value_import, 113 | } = Value::type_output(); 114 | import.extend(value_import); 115 | import.insert("builtins".into()); 116 | TypeInfo { 117 | name: format!("builtins.dict[{}, {}]", key_name, value_name), 118 | import, 119 | } 120 | } 121 | }; 122 | } 123 | 124 | impl PyStubType for BTreeMap { 125 | impl_map_inner!(); 126 | } 127 | 128 | impl PyStubType for HashMap { 129 | impl_map_inner!(); 130 | } 131 | 132 | impl PyStubType 133 | for indexmap::IndexMap 134 | { 135 | impl_map_inner!(); 136 | } 137 | 138 | macro_rules! impl_tuple { 139 | ($($T:ident),*) => { 140 | impl<$($T: PyStubType),*> PyStubType for ($($T),*) { 141 | fn type_output() -> TypeInfo { 142 | let mut merged = HashSet::new(); 143 | let mut names = Vec::new(); 144 | $( 145 | let TypeInfo { name, import } = $T::type_output(); 146 | names.push(name); 147 | merged.extend(import); 148 | )* 149 | TypeInfo { 150 | name: format!("tuple[{}]", names.join(", ")), 151 | import: merged, 152 | } 153 | } 154 | fn type_input() -> TypeInfo { 155 | let mut merged = HashSet::new(); 156 | let mut names = Vec::new(); 157 | $( 158 | let TypeInfo { name, import } = $T::type_input(); 159 | names.push(name); 160 | merged.extend(import); 161 | )* 162 | TypeInfo { 163 | name: format!("tuple[{}]", names.join(", ")), 164 | import: merged, 165 | } 166 | } 167 | } 168 | }; 169 | } 170 | 171 | impl_tuple!(T1, T2); 172 | impl_tuple!(T1, T2, T3); 173 | impl_tuple!(T1, T2, T3, T4); 174 | impl_tuple!(T1, T2, T3, T4, T5); 175 | impl_tuple!(T1, T2, T3, T4, T5, T6); 176 | impl_tuple!(T1, T2, T3, T4, T5, T6, T7); 177 | impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8); 178 | impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9); 179 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/stub_type/either.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use super::{ModuleRef, PyStubType, TypeInfo}; 4 | 5 | impl PyStubType for either::Either { 6 | fn type_input() -> TypeInfo { 7 | let TypeInfo { 8 | name: name_l, 9 | import: import_l, 10 | } = L::type_input(); 11 | let TypeInfo { 12 | name: name_r, 13 | import: import_r, 14 | } = R::type_input(); 15 | 16 | let mut import: HashSet = import_l.into_iter().chain(import_r).collect(); 17 | 18 | import.insert("typing".into()); 19 | 20 | TypeInfo { 21 | name: format!("typing.Union[{name_l}, {name_r}]"), 22 | import, 23 | } 24 | } 25 | fn type_output() -> TypeInfo { 26 | let TypeInfo { 27 | name: name_l, 28 | import: import_l, 29 | } = L::type_output(); 30 | let TypeInfo { 31 | name: name_r, 32 | import: import_r, 33 | } = R::type_output(); 34 | 35 | let mut import: HashSet = import_l.into_iter().chain(import_r).collect(); 36 | 37 | import.insert("typing".into()); 38 | 39 | TypeInfo { 40 | name: format!("typing.Union[{name_l}, {name_r}]"), 41 | import, 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/stub_type/numpy.rs: -------------------------------------------------------------------------------- 1 | use super::{PyStubType, TypeInfo}; 2 | use maplit::hashset; 3 | use numpy::{ 4 | ndarray::Dimension, Element, PyArray, PyArrayDescr, PyReadonlyArray, PyReadwriteArray, 5 | PyUntypedArray, 6 | }; 7 | 8 | trait NumPyScalar { 9 | fn type_() -> TypeInfo; 10 | } 11 | 12 | macro_rules! impl_numpy_scalar { 13 | ($ty:ty, $name:expr) => { 14 | impl NumPyScalar for $ty { 15 | fn type_() -> TypeInfo { 16 | TypeInfo { 17 | name: format!("numpy.{}", $name), 18 | import: hashset!["numpy".into()], 19 | } 20 | } 21 | } 22 | }; 23 | } 24 | 25 | impl_numpy_scalar!(i8, "int8"); 26 | impl_numpy_scalar!(i16, "int16"); 27 | impl_numpy_scalar!(i32, "int32"); 28 | impl_numpy_scalar!(i64, "int64"); 29 | impl_numpy_scalar!(u8, "uint8"); 30 | impl_numpy_scalar!(u16, "uint16"); 31 | impl_numpy_scalar!(u32, "uint32"); 32 | impl_numpy_scalar!(u64, "uint64"); 33 | impl_numpy_scalar!(f32, "float32"); 34 | impl_numpy_scalar!(f64, "float64"); 35 | impl_numpy_scalar!(num_complex::Complex32, "complex64"); 36 | impl_numpy_scalar!(num_complex::Complex64, "complex128"); 37 | 38 | impl PyStubType for PyArray { 39 | fn type_output() -> TypeInfo { 40 | let TypeInfo { name, mut import } = T::type_(); 41 | import.insert("numpy.typing".into()); 42 | TypeInfo { 43 | name: format!("numpy.typing.NDArray[{name}]"), 44 | import, 45 | } 46 | } 47 | } 48 | 49 | impl PyStubType for PyUntypedArray { 50 | fn type_output() -> TypeInfo { 51 | TypeInfo { 52 | name: "numpy.typing.NDArray[typing.Any]".into(), 53 | import: hashset!["numpy.typing".into(), "typing".into()], 54 | } 55 | } 56 | } 57 | 58 | impl PyStubType for PyReadonlyArray<'_, T, D> 59 | where 60 | T: NumPyScalar + Element, 61 | D: Dimension, 62 | { 63 | fn type_output() -> TypeInfo { 64 | PyArray::::type_output() 65 | } 66 | } 67 | 68 | impl PyStubType for PyReadwriteArray<'_, T, D> 69 | where 70 | T: NumPyScalar + Element, 71 | D: Dimension, 72 | { 73 | fn type_output() -> TypeInfo { 74 | PyArray::::type_output() 75 | } 76 | } 77 | 78 | impl PyStubType for PyArrayDescr { 79 | fn type_output() -> TypeInfo { 80 | TypeInfo { 81 | name: "numpy.dtype".into(), 82 | import: hashset!["numpy".into()], 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/stub_type/pyo3.rs: -------------------------------------------------------------------------------- 1 | use crate::stub_type::*; 2 | use ::pyo3::{ 3 | basic::CompareOp, 4 | pybacked::{PyBackedBytes, PyBackedStr}, 5 | pyclass::boolean_struct::False, 6 | types::*, 7 | Bound, Py, PyClass, PyRef, PyRefMut, 8 | }; 9 | use maplit::hashset; 10 | 11 | impl PyStubType for PyAny { 12 | fn type_output() -> TypeInfo { 13 | TypeInfo { 14 | name: "typing.Any".to_string(), 15 | import: hashset! { "typing".into() }, 16 | } 17 | } 18 | } 19 | 20 | impl PyStubType for Py { 21 | fn type_input() -> TypeInfo { 22 | T::type_input() 23 | } 24 | fn type_output() -> TypeInfo { 25 | T::type_output() 26 | } 27 | } 28 | 29 | impl PyStubType for PyRef<'_, T> { 30 | fn type_input() -> TypeInfo { 31 | T::type_input() 32 | } 33 | fn type_output() -> TypeInfo { 34 | T::type_output() 35 | } 36 | } 37 | 38 | impl> PyStubType for PyRefMut<'_, T> { 39 | fn type_input() -> TypeInfo { 40 | T::type_input() 41 | } 42 | fn type_output() -> TypeInfo { 43 | T::type_output() 44 | } 45 | } 46 | 47 | impl PyStubType for Bound<'_, T> { 48 | fn type_input() -> TypeInfo { 49 | T::type_input() 50 | } 51 | fn type_output() -> TypeInfo { 52 | T::type_output() 53 | } 54 | } 55 | 56 | macro_rules! impl_builtin { 57 | ($ty:ty, $pytype:expr) => { 58 | impl PyStubType for $ty { 59 | fn type_output() -> TypeInfo { 60 | TypeInfo { 61 | name: $pytype.to_string(), 62 | import: HashSet::new(), 63 | } 64 | } 65 | } 66 | }; 67 | } 68 | 69 | impl_builtin!(PyInt, "int"); 70 | impl_builtin!(PyFloat, "float"); 71 | impl_builtin!(PyList, "list"); 72 | impl_builtin!(PyTuple, "tuple"); 73 | impl_builtin!(PySlice, "slice"); 74 | impl_builtin!(PyDict, "dict"); 75 | impl_builtin!(PySet, "set"); 76 | impl_builtin!(PyString, "str"); 77 | impl_builtin!(PyBackedStr, "str"); 78 | impl_builtin!(PyByteArray, "bytearray"); 79 | impl_builtin!(PyBytes, "bytes"); 80 | impl_builtin!(PyBackedBytes, "bytes"); 81 | impl_builtin!(PyType, "type"); 82 | impl_builtin!(CompareOp, "int"); 83 | 84 | #[cfg_attr(all(not(pyo3_0_25), Py_LIMITED_API), expect(unused_macros))] 85 | macro_rules! impl_simple { 86 | ($ty:ty, $mod:expr, $pytype:expr) => { 87 | impl PyStubType for $ty { 88 | fn type_output() -> TypeInfo { 89 | TypeInfo { 90 | name: concat!($mod, ".", $pytype).to_string(), 91 | import: hashset! { $mod.into() }, 92 | } 93 | } 94 | } 95 | }; 96 | } 97 | 98 | #[cfg(any(pyo3_0_25, not(Py_LIMITED_API)))] 99 | impl_simple!(PyDate, "datetime", "date"); 100 | #[cfg(any(pyo3_0_25, not(Py_LIMITED_API)))] 101 | impl_simple!(PyDateTime, "datetime", "datetime"); 102 | #[cfg(any(pyo3_0_25, not(Py_LIMITED_API)))] 103 | impl_simple!(PyDelta, "datetime", "timedelta"); 104 | #[cfg(any(pyo3_0_25, not(Py_LIMITED_API)))] 105 | impl_simple!(PyTime, "datetime", "time"); 106 | #[cfg(any(pyo3_0_25, not(Py_LIMITED_API)))] 107 | impl_simple!(PyTzInfo, "datetime", "tzinfo"); 108 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/type_info.rs: -------------------------------------------------------------------------------- 1 | //! Store of metadata for generating Python stub file 2 | //! 3 | //! Stub file generation takes two steps: 4 | //! 5 | //! Store metadata (compile time) 6 | //! ------------------------------ 7 | //! Embed compile-time information about Rust types and PyO3 macro arguments 8 | //! using [inventory::submit!](https://docs.rs/inventory/latest/inventory/macro.submit.html) macro into source codes, 9 | //! and these information will be gathered by [inventory::iter](https://docs.rs/inventory/latest/inventory/struct.iter.html). 10 | //! This submodule is responsible for this process. 11 | //! 12 | //! - [PyClassInfo] stores information obtained from `#[pyclass]` macro 13 | //! - [PyMethodsInfo] stores information obtained from `#[pymethods]` macro 14 | //! 15 | //! and others are their components. 16 | //! 17 | //! Gathering metadata and generating stub file (runtime) 18 | //! ------------------------------------------------------- 19 | //! Since `#[pyclass]` and `#[pymethods]` definitions are not bundled in a single block, 20 | //! we have to reconstruct these information corresponding to a Python `class`. 21 | //! This process is done at runtime in [gen_stub](../../gen_stub) executable. 22 | //! 23 | 24 | use crate::{PyStubType, TypeInfo}; 25 | use std::any::TypeId; 26 | 27 | /// Work around for `CompareOp` for `__richcmp__` argument, 28 | /// which does not implements `FromPyObject` 29 | pub fn compare_op_type_input() -> TypeInfo { 30 | ::type_input() 31 | } 32 | 33 | pub fn no_return_type_output() -> TypeInfo { 34 | TypeInfo::none() 35 | } 36 | 37 | /// Info of method argument appears in `#[pymethods]` 38 | #[derive(Debug)] 39 | pub struct ArgInfo { 40 | pub name: &'static str, 41 | pub r#type: fn() -> TypeInfo, 42 | pub signature: Option, 43 | } 44 | #[derive(Debug, Clone)] 45 | pub enum SignatureArg { 46 | Ident, 47 | Assign { 48 | default: &'static std::sync::LazyLock, 49 | }, 50 | Star, 51 | Args, 52 | Keywords, 53 | } 54 | 55 | impl PartialEq for SignatureArg { 56 | #[inline] 57 | fn eq(&self, other: &Self) -> bool { 58 | match (self, other) { 59 | (Self::Assign { default: l_default }, Self::Assign { default: r_default }) => { 60 | let l_default: &String = l_default; 61 | let r_default: &String = r_default; 62 | l_default.eq(r_default) 63 | } 64 | _ => core::mem::discriminant(self) == core::mem::discriminant(other), 65 | } 66 | } 67 | } 68 | 69 | /// Type of a method 70 | #[derive(Debug, Clone, Copy, PartialEq)] 71 | pub enum MethodType { 72 | Instance, 73 | Static, 74 | Class, 75 | New, 76 | } 77 | 78 | /// Info of usual method appears in `#[pymethod]` 79 | #[derive(Debug)] 80 | pub struct MethodInfo { 81 | pub name: &'static str, 82 | pub args: &'static [ArgInfo], 83 | pub r#return: fn() -> TypeInfo, 84 | pub doc: &'static str, 85 | pub r#type: MethodType, 86 | } 87 | 88 | /// Info of getter method decorated with `#[getter]` or `#[pyo3(get, set)]` appears in `#[pyclass]` 89 | #[derive(Debug)] 90 | pub struct MemberInfo { 91 | pub name: &'static str, 92 | pub r#type: fn() -> TypeInfo, 93 | pub doc: &'static str, 94 | } 95 | 96 | /// Info of `#[pymethod]` 97 | #[derive(Debug)] 98 | pub struct PyMethodsInfo { 99 | // The Rust struct type-id of `impl` block where `#[pymethod]` acts on 100 | pub struct_id: fn() -> TypeId, 101 | /// Methods decorated with `#[getter]` 102 | pub getters: &'static [MemberInfo], 103 | /// Other usual methods 104 | pub methods: &'static [MethodInfo], 105 | } 106 | 107 | inventory::collect!(PyMethodsInfo); 108 | 109 | /// Info of `#[pyclass]` with Rust struct 110 | #[derive(Debug)] 111 | pub struct PyClassInfo { 112 | // Rust struct type-id 113 | pub struct_id: fn() -> TypeId, 114 | // The name exposed to Python 115 | pub pyclass_name: &'static str, 116 | /// Module name specified by `#[pyclass(module = "foo.bar")]` 117 | pub module: Option<&'static str>, 118 | /// Docstring 119 | pub doc: &'static str, 120 | /// static members by `#[pyo3(get, set)]` 121 | pub members: &'static [MemberInfo], 122 | /// Base classes specified by `#[pyclass(extends = Type)]` 123 | pub bases: &'static [fn() -> TypeInfo], 124 | } 125 | 126 | inventory::collect!(PyClassInfo); 127 | 128 | /// Info of `#[pyclass]` with Rust enum 129 | #[derive(Debug)] 130 | pub struct PyEnumInfo { 131 | // Rust struct type-id 132 | pub enum_id: fn() -> TypeId, 133 | // The name exposed to Python 134 | pub pyclass_name: &'static str, 135 | /// Module name specified by `#[pyclass(module = "foo.bar")]` 136 | pub module: Option<&'static str>, 137 | /// Docstring 138 | pub doc: &'static str, 139 | /// Variants of enum (name, doc) 140 | pub variants: &'static [(&'static str, &'static str)], 141 | } 142 | 143 | inventory::collect!(PyEnumInfo); 144 | 145 | /// Info of `#[pyfunction]` 146 | #[derive(Debug)] 147 | pub struct PyFunctionInfo { 148 | pub name: &'static str, 149 | pub args: &'static [ArgInfo], 150 | pub r#return: fn() -> TypeInfo, 151 | pub doc: &'static str, 152 | pub module: Option<&'static str>, 153 | } 154 | 155 | inventory::collect!(PyFunctionInfo); 156 | 157 | #[derive(Debug)] 158 | pub struct PyErrorInfo { 159 | pub name: &'static str, 160 | pub module: &'static str, 161 | pub base: fn() -> &'static str, 162 | } 163 | 164 | inventory::collect!(PyErrorInfo); 165 | 166 | #[derive(Debug)] 167 | pub struct PyVariableInfo { 168 | pub name: &'static str, 169 | pub module: &'static str, 170 | pub r#type: fn() -> TypeInfo, 171 | } 172 | 173 | inventory::collect!(PyVariableInfo); 174 | -------------------------------------------------------------------------------- /pyo3-stub-gen/src/util.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{prelude::*, types::*}; 2 | use std::{borrow::Cow, ffi::CString}; 3 | 4 | pub fn all_builtin_types(any: &Bound<'_, PyAny>) -> bool { 5 | if any.is_instance_of::() 6 | || any.is_instance_of::() 7 | || any.is_instance_of::() 8 | || any.is_instance_of::() 9 | || any.is_none() 10 | { 11 | return true; 12 | } 13 | if any.is_instance_of::() { 14 | return any 15 | .downcast::() 16 | .map(|dict| { 17 | dict.into_iter() 18 | .all(|(k, v)| all_builtin_types(&k) && all_builtin_types(&v)) 19 | }) 20 | .unwrap_or(false); 21 | } 22 | if any.is_instance_of::() { 23 | return any 24 | .downcast::() 25 | .map(|list| list.into_iter().all(|v| all_builtin_types(&v))) 26 | .unwrap_or(false); 27 | } 28 | if any.is_instance_of::() { 29 | return any 30 | .downcast::() 31 | .map(|list| list.into_iter().all(|v| all_builtin_types(&v))) 32 | .unwrap_or(false); 33 | } 34 | false 35 | } 36 | 37 | /// whether eval(repr(any)) == any 38 | pub fn valid_external_repr(any: &Bound<'_, PyAny>) -> Option { 39 | let globals = get_globals(any).ok()?; 40 | let fmt_str = any.repr().ok()?.to_string(); 41 | let fmt_cstr = CString::new(fmt_str.clone()).ok()?; 42 | let new_any = any.py().eval(&fmt_cstr, Some(&globals), None).ok()?; 43 | new_any.eq(any).ok() 44 | } 45 | 46 | fn get_globals<'py>(any: &Bound<'py, PyAny>) -> PyResult> { 47 | let type_object = any.get_type(); 48 | let type_name = type_object.getattr("__name__")?; 49 | let type_name: Cow = type_name.extract()?; 50 | let globals = PyDict::new(any.py()); 51 | globals.set_item(type_name, type_object)?; 52 | Ok(globals) 53 | } 54 | 55 | pub fn fmt_py_obj<'py, T: pyo3::IntoPyObjectExt<'py>>(py: Python<'py>, obj: T) -> String { 56 | if let Ok(any) = obj.into_bound_py_any(py) { 57 | if all_builtin_types(&any) || valid_external_repr(&any).is_some_and(|valid| valid) { 58 | if let Ok(py_str) = any.repr() { 59 | return py_str.to_string(); 60 | } 61 | } 62 | } 63 | "...".to_owned() 64 | } 65 | 66 | #[cfg(test)] 67 | mod test { 68 | use super::*; 69 | #[pyclass] 70 | #[derive(Debug)] 71 | struct A {} 72 | #[test] 73 | fn test_fmt_dict() { 74 | pyo3::prepare_freethreaded_python(); 75 | Python::with_gil(|py| { 76 | let dict = PyDict::new(py); 77 | _ = dict.set_item("k1", "v1"); 78 | _ = dict.set_item("k2", 2); 79 | assert_eq!("{'k1': 'v1', 'k2': 2}", fmt_py_obj(py, &dict)); 80 | // class A variable can not be formatted 81 | _ = dict.set_item("k3", A {}); 82 | assert_eq!("...", fmt_py_obj(py, &dict)); 83 | }) 84 | } 85 | #[test] 86 | fn test_fmt_list() { 87 | pyo3::prepare_freethreaded_python(); 88 | Python::with_gil(|py| { 89 | let list = PyList::new(py, [1, 2]).unwrap(); 90 | assert_eq!("[1, 2]", fmt_py_obj(py, &list)); 91 | // class A variable can not be formatted 92 | let list = PyList::new(py, [A {}, A {}]).unwrap(); 93 | assert_eq!("...", fmt_py_obj(py, &list)); 94 | }) 95 | } 96 | #[test] 97 | fn test_fmt_tuple() { 98 | pyo3::prepare_freethreaded_python(); 99 | Python::with_gil(|py| { 100 | let tuple = PyTuple::new(py, [1, 2]).unwrap(); 101 | assert_eq!("(1, 2)", fmt_py_obj(py, tuple)); 102 | let tuple = PyTuple::new(py, [1]).unwrap(); 103 | assert_eq!("(1,)", fmt_py_obj(py, tuple)); 104 | // class A variable can not be formatted 105 | let tuple = PyTuple::new(py, [A {}]).unwrap(); 106 | assert_eq!("...", fmt_py_obj(py, tuple)); 107 | }) 108 | } 109 | #[test] 110 | fn test_fmt_other() { 111 | pyo3::prepare_freethreaded_python(); 112 | Python::with_gil(|py| { 113 | // str 114 | assert_eq!("'123'", fmt_py_obj(py, "123")); 115 | assert_eq!("\"don't\"", fmt_py_obj(py, "don't")); 116 | assert_eq!("'str\\\\'", fmt_py_obj(py, "str\\")); 117 | // bool 118 | assert_eq!("True", fmt_py_obj(py, true)); 119 | assert_eq!("False", fmt_py_obj(py, false)); 120 | // int 121 | assert_eq!("123", fmt_py_obj(py, 123)); 122 | // float 123 | assert_eq!("1.23", fmt_py_obj(py, 1.23)); 124 | // None 125 | let none: Option = None; 126 | assert_eq!("None", fmt_py_obj(py, none)); 127 | // class A variable can not be formatted 128 | assert_eq!("...", fmt_py_obj(py, A {})); 129 | }) 130 | } 131 | #[test] 132 | fn test_fmt_enum() { 133 | #[pyclass(eq, eq_int)] 134 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 135 | pub enum Number { 136 | Float, 137 | Integer, 138 | } 139 | pyo3::prepare_freethreaded_python(); 140 | Python::with_gil(|py| { 141 | assert_eq!("Number.Float", fmt_py_obj(py, Number::Float)); 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.uv.workspace] 2 | members = [ 3 | "examples/pure_abi3", 4 | "examples/pure", 5 | "examples/mixed", 6 | "examples/mixed_sub", 7 | "examples/mixed_sub_multiple", 8 | ] 9 | 10 | [dependency-groups] 11 | dev = ["pyright>=1.1.400", "pytest>=8.3.5"] 12 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.9" 4 | 5 | [manifest] 6 | members = [ 7 | "mixed", 8 | "mixed-sub", 9 | "mixed-sub-multiple", 10 | "pure", 11 | "pure-abi3", 12 | ] 13 | 14 | [manifest.dependency-groups] 15 | dev = [ 16 | { name = "pyright", specifier = ">=1.1.400" }, 17 | { name = "pytest", specifier = ">=8.3.5" }, 18 | ] 19 | 20 | [[package]] 21 | name = "colorama" 22 | version = "0.4.6" 23 | source = { registry = "https://pypi.org/simple" } 24 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 25 | wheels = [ 26 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 27 | ] 28 | 29 | [[package]] 30 | name = "exceptiongroup" 31 | version = "1.2.2" 32 | source = { registry = "https://pypi.org/simple" } 33 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } 34 | wheels = [ 35 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, 36 | ] 37 | 38 | [[package]] 39 | name = "iniconfig" 40 | version = "2.1.0" 41 | source = { registry = "https://pypi.org/simple" } 42 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 43 | wheels = [ 44 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 45 | ] 46 | 47 | [[package]] 48 | name = "mixed" 49 | version = "0.1" 50 | source = { editable = "examples/mixed" } 51 | 52 | [[package]] 53 | name = "mixed-sub" 54 | version = "0.1" 55 | source = { editable = "examples/mixed_sub" } 56 | 57 | [[package]] 58 | name = "mixed-sub-multiple" 59 | version = "0.1" 60 | source = { editable = "examples/mixed_sub_multiple" } 61 | 62 | [[package]] 63 | name = "nodeenv" 64 | version = "1.9.1" 65 | source = { registry = "https://pypi.org/simple" } 66 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } 67 | wheels = [ 68 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, 69 | ] 70 | 71 | [[package]] 72 | name = "packaging" 73 | version = "25.0" 74 | source = { registry = "https://pypi.org/simple" } 75 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 76 | wheels = [ 77 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 78 | ] 79 | 80 | [[package]] 81 | name = "pluggy" 82 | version = "1.5.0" 83 | source = { registry = "https://pypi.org/simple" } 84 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 85 | wheels = [ 86 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 87 | ] 88 | 89 | [[package]] 90 | name = "pure" 91 | version = "0.1" 92 | source = { editable = "examples/pure" } 93 | 94 | [[package]] 95 | name = "pure-abi3" 96 | version = "0.1" 97 | source = { editable = "examples/pure_abi3" } 98 | 99 | [[package]] 100 | name = "pyright" 101 | version = "1.1.400" 102 | source = { registry = "https://pypi.org/simple" } 103 | dependencies = [ 104 | { name = "nodeenv" }, 105 | { name = "typing-extensions" }, 106 | ] 107 | sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546, upload-time = "2025-04-24T12:55:18.907Z" } 108 | wheels = [ 109 | { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460, upload-time = "2025-04-24T12:55:17.002Z" }, 110 | ] 111 | 112 | [[package]] 113 | name = "pytest" 114 | version = "8.3.5" 115 | source = { registry = "https://pypi.org/simple" } 116 | dependencies = [ 117 | { name = "colorama", marker = "sys_platform == 'win32'" }, 118 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 119 | { name = "iniconfig" }, 120 | { name = "packaging" }, 121 | { name = "pluggy" }, 122 | { name = "tomli", marker = "python_full_version < '3.11'" }, 123 | ] 124 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 125 | wheels = [ 126 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 127 | ] 128 | 129 | [[package]] 130 | name = "tomli" 131 | version = "2.2.1" 132 | source = { registry = "https://pypi.org/simple" } 133 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 134 | wheels = [ 135 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 136 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 137 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 138 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 139 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 140 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 141 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 142 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 143 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 144 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 145 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 146 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 147 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 148 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 149 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 150 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 151 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 152 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 153 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 154 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 155 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 156 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 157 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 158 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 159 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 160 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 161 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 162 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 163 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 164 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 165 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 166 | ] 167 | 168 | [[package]] 169 | name = "typing-extensions" 170 | version = "4.13.2" 171 | source = { registry = "https://pypi.org/simple" } 172 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 173 | wheels = [ 174 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 175 | ] 176 | --------------------------------------------------------------------------------