├── .bazelignore ├── .bazelrc ├── .bazelversion ├── .bcr ├── README.md ├── config.yml ├── metadata.template.json ├── presubmit.yml └── source.template.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── renovate.json ├── reviewers.json └── workflows │ ├── automation-autorelease.yml │ ├── automation-autosquash.yml │ ├── automation-reviewbot.yml │ ├── automation-update-requirements.yml │ ├── ci.bazelrc │ ├── ci.yml │ ├── release.yml │ └── release_prep.sh ├── .gitignore ├── .vscode └── settings.json ├── BUILD.bazel ├── LICENSE ├── MODULE.bazel ├── WORKSPACE.bazel ├── mypy ├── BUILD.bazel ├── mypy.bzl ├── private │ ├── BUILD.bazel │ ├── default_mypy.ini │ ├── mypy.bzl │ ├── mypy_runner.py │ ├── py_type_library.bzl │ ├── py_type_library.py │ ├── requirements.in │ ├── requirements.txt │ └── types.bzl ├── py_type_library.bzl └── types.bzl └── readme.md /.bazelignore: -------------------------------------------------------------------------------- 1 | examples/ -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | common --enable_bzlmod 2 | 3 | common --lockfile_mode=off 4 | 5 | test --test_output=errors 6 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 7.1.0 2 | -------------------------------------------------------------------------------- /.bcr/README.md: -------------------------------------------------------------------------------- 1 | # Bazel Central Registry 2 | 3 | When the ruleset is released, we want it to be published to the 4 | Bazel Central Registry automatically: 5 | 6 | 7 | This folder contains configuration files to automate the publish step. 8 | See 9 | for authoritative documentation about these files. 10 | -------------------------------------------------------------------------------- /.bcr/config.yml: -------------------------------------------------------------------------------- 1 | fixedReleaser: 2 | login: mark-thm 3 | email: 123787712+mark-thm@users.noreply.github.com 4 | -------------------------------------------------------------------------------- /.bcr/metadata.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://github.com/theoremlp/rules_mypy", 3 | "maintainers": [ 4 | { 5 | "email": "123787712+mark-thm@users.noreply.github.com", 6 | "github": "mark-thm", 7 | "name": "Mark Elliot" 8 | }, 9 | { 10 | "email": "bazel-maintainers@theoremlp.com", 11 | "github": "theoremlp", 12 | "name": "Theorem Bazel Maintainers" 13 | } 14 | ], 15 | "repository": ["github:theoremlp/rules_mypy"], 16 | "versions": [], 17 | "yanked_versions": {} 18 | } 19 | -------------------------------------------------------------------------------- /.bcr/presubmit.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | platform: 3 | - debian10 4 | - ubuntu2004 5 | - macos 6 | - macos_arm64 7 | bazel: 8 | - 7.x 9 | tasks: 10 | verify_targets: 11 | name: Verify build targets 12 | platform: ${{ platform }} 13 | bazel: ${{ bazel }} 14 | build_targets: 15 | - "@rules_mypy//..." 16 | -------------------------------------------------------------------------------- /.bcr/source.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrity": "**leave this alone**", 3 | "strip_prefix": "{REPO}-{VERSION}", 4 | "url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/{REPO}-{VERSION}.tar.gz" 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # In code review, collapse generated files 2 | docs/*.md linguist-generated=true 3 | 4 | ################################# 5 | # Configuration for 'git archive' 6 | # See https://git-scm.com/docs/git-archive#ATTRIBUTES 7 | 8 | # Don't include examples in the distribution artifact, to reduce size. 9 | # You may want to add additional exclusions for folders or files that users don't need. 10 | examples export-ignore 11 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @theoremlp/eng 2 | 3 | .github/workflows/*.yml @theoremlp/eng @reviewbot-theorem 4 | uv/private/uv.lock.json @theoremlp/eng @reviewbot-theorem 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":semanticCommitsDisabled" 5 | ], 6 | "enabledManagers": [ 7 | "github-actions", 8 | "pip_requirements" 9 | ], 10 | "timezone": "America/New_York", 11 | "schedule": [ 12 | "every weekday after 9am before 5pm" 13 | ], 14 | "branchConcurrentLimit": 10, 15 | "labels": [ 16 | "automerge" 17 | ], 18 | "dependencyDashboard": true, 19 | "packageRules": [ 20 | { 21 | "matchFileNames": [ 22 | "MODULE.bazel" 23 | ], 24 | "enabled": false 25 | } 26 | ], 27 | "pip_requirements": { 28 | "fileMatch": [ 29 | "mypy/private/requirements.in" 30 | ], 31 | "ignorePaths": [ 32 | "examples/demo/requirements.in", 33 | "examples/demo/requirements.txt", 34 | "mypy/private/requirements.txt" 35 | ], 36 | "rangeStrategy": "bump" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/reviewers.json: -------------------------------------------------------------------------------- 1 | { 2 | "teams": {}, 3 | "reviewers": {}, 4 | "overrides": [ 5 | { 6 | "description": "Auto-approve automated PRs", 7 | "onlyModifiedByUsers": ["thm-automation[bot]", "renovate-thm[bot]"], 8 | "onlyModifiedFileRegExs": [ 9 | "^.github/workflows/.*", 10 | "^mypy/private/requirements.(in|txt)$" 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/automation-autorelease.yml: -------------------------------------------------------------------------------- 1 | name: autorelease 2 | on: 3 | workflow_dispatch: {} 4 | schedule: 5 | # check at 11am every day 6 | - cron: "0 11 * * *" 7 | jobs: 8 | autorelease: 9 | name: autorelease 10 | runs-on: ubuntu-latest 11 | if: ${{ github.ref == 'refs/heads/main' }} 12 | steps: 13 | - name: Get Token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: ${{ secrets.THM_AUTOMATION_APP_ID }} 18 | private-key: ${{ secrets.THM_AUTOMATION_PRIVATE_KEY }} 19 | - name: autorelease 20 | uses: markelliot/autorelease@v2 21 | with: 22 | github-token: ${{ steps.app-token.outputs.token }} 23 | max-days: 7 24 | tag-only: true 25 | -------------------------------------------------------------------------------- /.github/workflows/automation-autosquash.yml: -------------------------------------------------------------------------------- 1 | name: autosquash 2 | on: 3 | pull_request: 4 | types: 5 | # omit "opened" as labeling happens after opening 6 | # and when we include both the two events end up 7 | # canceling each other's runs. 8 | - reopened 9 | - edited 10 | - labeled 11 | - synchronize 12 | - unlabeled 13 | - ready_for_review 14 | concurrency: 15 | # only one autosquash at a time per PR 16 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 17 | cancel-in-progress: true 18 | jobs: 19 | autosquash: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Get Token 23 | if: github.event.pull_request.head.repo.full_name == 'theoremlp/rules_mypy' 24 | id: app-token 25 | uses: actions/create-github-app-token@v2 26 | with: 27 | app-id: ${{ secrets.THM_AUTOMATION_APP_ID }} 28 | private-key: ${{ secrets.THM_AUTOMATION_PRIVATE_KEY }} 29 | - if: github.event.pull_request.head.repo.full_name == 'theoremlp/rules_mypy' 30 | uses: actions/checkout@v4 31 | - if: github.event.pull_request.head.repo.full_name == 'theoremlp/rules_mypy' 32 | uses: theoremlp/autosquash@v1 33 | with: 34 | github-token: ${{ steps.app-token.outputs.token }} 35 | pull-request-number: ${{ github.event.pull_request.number }} 36 | squash-commit-title: "${{ github.event.pull_request.title }} (#${{ github.event.pull_request.number }})" 37 | squash-commit-message: "${{ github.event.pull_request.body }}" 38 | do-not-merge-label: "do not merge" 39 | required-label: "automerge" 40 | -------------------------------------------------------------------------------- /.github/workflows/automation-reviewbot.yml: -------------------------------------------------------------------------------- 1 | name: reviewbot 2 | on: 3 | pull_request: {} 4 | pull_request_review: {} 5 | jobs: 6 | required-reviewers: 7 | name: reviewbot 8 | runs-on: ubuntu-latest 9 | if: github.event.pull_request.head.repo.full_name == 'theoremlp/rules_mypy' 10 | steps: 11 | - name: required-reviewers 12 | uses: theoremlp/required-reviews@v2 13 | with: 14 | github-token: ${{ secrets.REVIEW_TOKEN_PUB }} 15 | post-review: true 16 | -------------------------------------------------------------------------------- /.github/workflows/automation-update-requirements.yml: -------------------------------------------------------------------------------- 1 | name: Update requirements lockfiles 2 | on: 3 | pull_request: 4 | paths: 5 | - "mypy/private/requirements.in" 6 | - "examples/demo/requirements.in" 7 | jobs: 8 | update-requirements: 9 | name: Update requirements lockfiles 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write 13 | contents: read 14 | steps: 15 | - name: Get Token 16 | id: app-token 17 | uses: actions/create-github-app-token@v2 18 | with: 19 | app-id: ${{ secrets.THM_AUTOMATION_APP_ID }} 20 | private-key: ${{ secrets.THM_AUTOMATION_PRIVATE_KEY }} 21 | - uses: actions/checkout@v4 22 | with: 23 | token: ${{ steps.app-token.outputs.token }} 24 | ref: ${{ github.event.pull_request.head.sha }} 25 | - name: Update requirements lockfiles 26 | run: bazel run //mypy/private:generate_requirements_lock 27 | - name: Update requirements lockfiles (examples/demo) 28 | working-directory: examples/demo 29 | run: bazel run //:generate_requirements_lock 30 | - name: Commit 31 | run: | 32 | if [[ -n "$( git diff \ 33 | "mypy/private/requirements.in" \ 34 | "mypy/private/requirements.txt" \ 35 | "examples/demo/requirements.in" \ 36 | "examples/demo/requirements.txt" \ 37 | )" ]] 38 | then 39 | git config --local user.name 'Theorem Automation' 40 | git config --local user.email 'thm-automation[bot]@users.noreply.github.com' 41 | git checkout -b thm-automation/tmp 42 | git add "mypy/private/requirements.in" 43 | git add "mypy/private/requirements.txt" 44 | git add "examples/demo/requirements.in" 45 | git add "examples/demo/requirements.txt" 46 | git commit -m "Update requirements lockfiles" 47 | git push origin "HEAD:$GITHUB_HEAD_REF" 48 | fi 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.bazelrc: -------------------------------------------------------------------------------- 1 | # This file contains Bazel settings to apply on CI only. 2 | # It is referenced with a --bazelrc option in the call to bazel in ci.yaml 3 | 4 | # Debug where options came from 5 | build --announce_rc 6 | 7 | # This directory is configured in GitHub actions to be persisted between runs. 8 | # We do not enable the repository cache to cache downloaded external artifacts 9 | # as these are generally faster to download again than to fetch them from the 10 | # GitHub actions cache. 11 | build --disk_cache=~/.cache/bazel 12 | 13 | # Don't rely on test logs being easily accessible from the test runner, 14 | # though it makes the log noisier. 15 | test --test_output=errors 16 | 17 | # Allows tests to run bazelisk-in-bazel, since this is the cache folder used 18 | test --test_env=XDG_CACHE_HOME 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | folder: [".", "examples/demo", "examples/opt-in"] 16 | version: ["7.1.0", "8.0.0"] 17 | os: ["ubuntu-latest"] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: bazel-contrib/setup-bazel@0.15.0 22 | with: 23 | repository-cache: true 24 | bazelrc: common --announce_rc --color=yes --enable_workspace=false 25 | - name: Configure Bazel version 26 | working-directory: ${{ matrix.folder }} 27 | run: | 28 | echo "${{ matrix.version }}" > .bazelversion 29 | bazel version 30 | - name: Build 31 | working-directory: ${{ matrix.folder }} 32 | run: bazel build ... 33 | - name: Test 34 | working-directory: ${{ matrix.folder }} 35 | run: bazel test ... 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | 7 | jobs: 8 | release: 9 | uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v7 10 | permissions: 11 | contents: write 12 | with: 13 | release_files: rules_mypy-*.tar.gz 14 | prerelease: false 15 | -------------------------------------------------------------------------------- /.github/workflows/release_prep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # invoked by release workflow 4 | # (via https://github.com/bazel-contrib/.github/blob/master/.github/workflows/release_ruleset.yaml) 5 | 6 | set -o errexit -o nounset -o pipefail 7 | 8 | RULES_NAME="rules_mypy" 9 | TAG="${GITHUB_REF_NAME}" 10 | PREFIX="${RULES_NAME}-${TAG:1}" 11 | ARCHIVE="${RULES_NAME}-${TAG:1}.tar.gz" 12 | 13 | # embed version in MODULE.bazel 14 | perl -pi -e "s/version = \"0\.0\.0\",/version = \"${TAG:1}\",/g" MODULE.bazel 15 | 16 | stash_name=`git stash create`; 17 | git archive --format=tar --prefix=${PREFIX}/ "${stash_name}" | gzip > $ARCHIVE 18 | 19 | SHA=$(shasum -a 256 $ARCHIVE | awk '{print $1}') 20 | 21 | cat << EOF 22 | ## Using Bzlmod with Bazel 7 23 | 24 | 1. Enable with \`common --enable_bzlmod\` in \`.bazelrc\`. 25 | 2. Add to your \`MODULE.bazel\` file: 26 | 27 | \`\`\`starlark 28 | bazel_dep(name = "${RULES_NAME}", version = "${TAG:1}") 29 | \`\`\` 30 | EOF 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-bin/ 2 | bazel-out/ 3 | bazel-testlogs/ 4 | bazel-* 5 | 6 | venv/ 7 | .venv/ 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic" 3 | } 4 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@buildifier_prebuilt//:rules.bzl", "buildifier", "buildifier_test") 2 | 3 | exports_files([ 4 | "MODULE.bazel", 5 | ]) 6 | 7 | buildifier( 8 | name = "buildifier.fix", 9 | exclude_patterns = ["./.git/*"], 10 | lint_mode = "fix", 11 | mode = "fix", 12 | visibility = ["//thm/buildtools/fix:__pkg__"], 13 | ) 14 | 15 | buildifier_test( 16 | name = "buildifier.test", 17 | exclude_patterns = ["./.git/*"], 18 | lint_mode = "warn", 19 | mode = "diff", 20 | no_sandbox = True, 21 | workspace = "//:MODULE.bazel", 22 | ) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | "rules_mypy" 2 | 3 | module( 4 | name = "rules_mypy", 5 | version = "0.0.0", 6 | ) 7 | 8 | bazel_dep(name = "bazel_skylib", version = "1.4.1") 9 | bazel_dep(name = "buildifier_prebuilt", version = "7.3.1") 10 | bazel_dep(name = "platforms", version = "0.0.8") 11 | bazel_dep(name = "rules_python", version = "1.1.0") 12 | bazel_dep(name = "rules_uv", version = "0.21.0") 13 | 14 | # configuration 15 | PYTHON_VERSION = "3.12" 16 | 17 | python = use_extension("@rules_python//python/extensions:python.bzl", "python") 18 | python.toolchain( 19 | is_default = True, 20 | python_version = PYTHON_VERSION, 21 | ) 22 | 23 | pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") 24 | pip.parse( 25 | enable_implicit_namespace_pkgs = True, 26 | hub_name = "rules_mypy_pip", 27 | python_version = PYTHON_VERSION, 28 | requirements_by_platform = { 29 | "//mypy/private:requirements.txt": "*", 30 | }, 31 | ) 32 | use_repo(pip, "rules_mypy_pip") 33 | -------------------------------------------------------------------------------- /WORKSPACE.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theoremlp/rules_mypy/8ecc4fa2636babfc94773d24d5f3e1b5cd170a48/WORKSPACE.bazel -------------------------------------------------------------------------------- /mypy/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | 3 | bzl_library( 4 | name = "mypy", 5 | srcs = ["mypy.bzl"], 6 | visibility = ["//visibility:public"], 7 | ) 8 | 9 | bzl_library( 10 | name = "py_type_library", 11 | srcs = ["py_type_library.bzl"], 12 | visibility = ["//visibility:public"], 13 | ) 14 | 15 | bzl_library( 16 | name = "types", 17 | srcs = ["types.bzl"], 18 | visibility = ["//visibility:public"], 19 | ) 20 | -------------------------------------------------------------------------------- /mypy/mypy.bzl: -------------------------------------------------------------------------------- 1 | "Public API for interacting with the mypy rule." 2 | 3 | load("//mypy/private:mypy.bzl", _mypy = "mypy", _mypy_cli = "mypy_cli") 4 | 5 | # re-export mypy aspect factory 6 | mypy = _mypy 7 | 8 | # export custom mypy_cli producer 9 | mypy_cli = _mypy_cli 10 | -------------------------------------------------------------------------------- /mypy/private/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_skylib//:bzl_library.bzl", "bzl_library") 2 | load("@rules_mypy_pip//:requirements.bzl", "requirement") 3 | load("@rules_python//python:py_binary.bzl", "py_binary") 4 | load("@rules_uv//uv:pip.bzl", "pip_compile") 5 | load("@rules_uv//uv:venv.bzl", "create_venv") 6 | load(":mypy.bzl", "mypy_cli") 7 | 8 | exports_files([ 9 | "mypy_runner.py", 10 | "default_mypy.ini", 11 | ]) 12 | 13 | bzl_library( 14 | name = "mypy_rules", 15 | srcs = ["mypy.bzl"], 16 | visibility = ["//mypy:__subpackages__"], 17 | ) 18 | 19 | bzl_library( 20 | name = "py_type_library_rules", 21 | srcs = ["py_type_library.bzl"], 22 | visibility = ["//mypy:__subpackages__"], 23 | ) 24 | 25 | bzl_library( 26 | name = "types_rules", 27 | srcs = ["types.bzl"], 28 | visibility = ["//mypy:__subpackages__"], 29 | ) 30 | 31 | pip_compile( 32 | name = "generate_requirements_lock", 33 | requirements_in = "requirements.in", 34 | requirements_txt = "requirements.txt", 35 | ) 36 | 37 | create_venv( 38 | name = "venv", 39 | requirements_txt = "requirements.txt", 40 | ) 41 | 42 | mypy_cli(name = "mypy") 43 | 44 | py_binary( 45 | name = "py_type_library", 46 | srcs = ["py_type_library.py"], 47 | main = "py_type_library.py", 48 | python_version = "3.12", 49 | visibility = ["//visibility:public"], 50 | deps = [ 51 | requirement("click"), 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /mypy/private/default_mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | # make command line output easier to read 4 | pretty = True 5 | 6 | # do not produce errors for imported code 7 | # (only produce errors for the explicitly listed files) 8 | follow_imports = silent 9 | 10 | # follow imports to discover type stubs 11 | follow_imports_for_stubs = True 12 | -------------------------------------------------------------------------------- /mypy/private/mypy.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | mypy aspect. 3 | 4 | The mypy aspect runs mypy, succeeding if mypy is error free and failing if mypy produces errors. The 5 | result of the aspect is a mypy cache directory, located at [name].mypy_cache. When provided input cache 6 | directories (the results of other mypy builds), the underlying action first attempts to merge the cache 7 | directories. 8 | """ 9 | 10 | load("@rules_mypy_pip//:requirements.bzl", "requirement") 11 | load("@rules_python//python:py_binary.bzl", "py_binary") 12 | load("@rules_python//python:py_info.bzl", RulesPythonPyInfo = "PyInfo") 13 | load(":py_type_library.bzl", "PyTypeLibraryInfo") 14 | 15 | MypyCacheInfo = provider( 16 | doc = "Output details of the mypy build rule.", 17 | fields = { 18 | "directory": "Location of the mypy cache produced by this target.", 19 | }, 20 | ) 21 | 22 | def _extract_import_dir(import_): 23 | # _main/path/to/package -> path/to/package 24 | return import_.split("/", 1)[-1] 25 | 26 | def _imports(target): 27 | if RulesPythonPyInfo in target: 28 | return target[RulesPythonPyInfo].imports.to_list() 29 | elif PyInfo in target: 30 | return target[PyInfo].imports.to_list() 31 | else: 32 | return [] 33 | 34 | def _extract_imports(target): 35 | return [_extract_import_dir(i) for i in _imports(target)] 36 | 37 | def _opt_out(opt_out_tags, rule_tags): 38 | "Returns true iff at least one opt_out_tag appears in rule_tags." 39 | if len(opt_out_tags) == 0: 40 | return False 41 | 42 | for tag in opt_out_tags: 43 | if tag in rule_tags: 44 | return True 45 | 46 | return False 47 | 48 | def _opt_in(opt_in_tags, rule_tags): 49 | "Returns true iff opt_in_tags is empty or at least one of opt_in_tags appears in rule_tags." 50 | if len(opt_in_tags) == 0: 51 | return True 52 | 53 | for tag in opt_in_tags: 54 | if tag in rule_tags: 55 | return True 56 | 57 | return False 58 | 59 | def _mypy_impl(target, ctx): 60 | # skip non-root targets 61 | if target.label.workspace_root != "": 62 | return [] 63 | 64 | if RulesPythonPyInfo not in target and PyInfo not in target: 65 | return [] 66 | 67 | # disable if a target is tagged with at least one suppression tag 68 | if _opt_out(ctx.attr._suppression_tags, ctx.rule.attr.tags): 69 | return [] 70 | 71 | # disable if there are opt-in tags and one is not present 72 | if not _opt_in(ctx.attr._opt_in_tags, ctx.rule.attr.tags): 73 | return [] 74 | 75 | # ignore rules that don't carry source files like py_proto_library 76 | if not hasattr(ctx.rule.files, "srcs"): 77 | return [] 78 | 79 | # we need to help mypy map the location of external deps by setting 80 | # MYPYPATH to include the site-packages directories. 81 | external_deps = {} 82 | 83 | # we need to help mypy map the location of first party deps with custom 84 | # 'imports' by setting MYPYPATH. 85 | imports_dirs = {} 86 | 87 | # generated dirs 88 | generated_dirs = {} 89 | 90 | upstream_caches = [] 91 | 92 | types = [] 93 | 94 | depsets = [] 95 | 96 | type_mapping = dict(zip([k.label for k in ctx.attr._types_keys], ctx.attr._types_values)) 97 | dep_with_stubs = [_.label.workspace_root + "/site-packages" for _ in ctx.attr._types_keys] 98 | additional_types = [ 99 | type_mapping[dep.label] 100 | for dep in ctx.rule.attr.deps 101 | if dep.label in type_mapping 102 | ] 103 | 104 | for import_ in _extract_imports(target): 105 | imports_dirs[import_] = 1 106 | 107 | pyi_files = [] 108 | pyi_dirs = {} 109 | for dep in (ctx.rule.attr.deps + additional_types): 110 | if RulesPythonPyInfo in dep and hasattr(dep[RulesPythonPyInfo], "direct_pyi_files"): 111 | pyi_files.extend(dep[RulesPythonPyInfo].direct_pyi_files.to_list()) 112 | pyi_dirs |= {"%s/%s" % (ctx.bin_dir.path, imp): None for imp in _extract_imports(dep) if imp != "site-packages" and imp != "_main"} 113 | depsets.append(dep.default_runfiles.files) 114 | if PyTypeLibraryInfo in dep: 115 | types.append(dep[PyTypeLibraryInfo].directory.path + "/site-packages") 116 | elif dep.label in type_mapping: 117 | continue 118 | elif dep.label.workspace_root.startswith("external/"): 119 | # TODO: do we need this, still? 120 | external_deps[dep.label.workspace_root + "/site-packages"] = 1 121 | for imp in [_ for _ in _imports(dep) if "mypy_extensions" not in _ and "typing_extensions" not in _]: 122 | path = "external/{}".format(imp) 123 | if path not in dep_with_stubs: 124 | external_deps[path] = 1 125 | elif dep.label.workspace_name == "": 126 | for import_ in _extract_imports(dep): 127 | imports_dirs[import_] = 1 128 | 129 | if MypyCacheInfo in dep: 130 | upstream_caches.append(dep[MypyCacheInfo].directory) 131 | 132 | for file in dep.default_runfiles.files.to_list(): 133 | if file.root.path: 134 | generated_dirs[file.root.path] = 1 135 | 136 | # TODO: can we use `ctx.bin_dir.path` here to cover generated files 137 | # and as a way to skip iterating over depset contents to find generated 138 | # file roots? 139 | 140 | generated_imports_dirs = [] 141 | for generated_dir in generated_dirs.keys(): 142 | for import_ in imports_dirs.keys(): 143 | generated_imports_dirs.append("{}/{}".format(generated_dir, import_)) 144 | 145 | # types need to appear first in the mypy path since the module directories 146 | # are the same and mypy resolves the first ones, first. 147 | mypy_path = ":".join(sorted(types) + sorted(external_deps) + sorted(imports_dirs) + sorted(generated_dirs) + sorted(generated_imports_dirs) + sorted(pyi_dirs)) 148 | 149 | output_file = ctx.actions.declare_file(ctx.rule.attr.name + ".mypy_stdout") 150 | 151 | args = ctx.actions.args() 152 | args.add("--output", output_file) 153 | 154 | result_info = [OutputGroupInfo(mypy = depset([output_file]))] 155 | if ctx.attr.cache: 156 | cache_directory = ctx.actions.declare_directory(ctx.rule.attr.name + ".mypy_cache") 157 | args.add("--cache-dir", cache_directory.path) 158 | 159 | outputs = [output_file, cache_directory] 160 | result_info.append(MypyCacheInfo(directory = cache_directory)) 161 | else: 162 | outputs = [output_file] 163 | 164 | args.add_all([c.path for c in upstream_caches], before_each = "--upstream-cache") 165 | args.add_all([s for s in ctx.rule.files.srcs if "/_virtual_imports/" not in s.short_path]) 166 | 167 | if hasattr(ctx.attr, "_mypy_ini"): 168 | args.add("--mypy-ini", ctx.file._mypy_ini.path) 169 | config_files = [ctx.file._mypy_ini] 170 | else: 171 | config_files = [] 172 | 173 | extra_env = {} 174 | if ctx.attr.color: 175 | # force color on 176 | extra_env["MYPY_FORCE_COLOR"] = "1" 177 | 178 | # force color on only works if TERM is set to something that supports color 179 | extra_env["TERM"] = "xterm-256color" 180 | 181 | py_type_files = [x for x in ctx.rule.files.data if x.basename == "py.typed" or x.extension == "pyi"] 182 | ctx.actions.run( 183 | mnemonic = "mypy", 184 | progress_message = "mypy %{label}", 185 | inputs = depset( 186 | direct = ctx.rule.files.srcs + py_type_files + pyi_files + upstream_caches + config_files, 187 | transitive = depsets, 188 | ), 189 | outputs = outputs, 190 | executable = ctx.executable._mypy_cli, 191 | arguments = [args], 192 | env = {"MYPYPATH": mypy_path} | ctx.configuration.default_shell_env | extra_env, 193 | ) 194 | 195 | return result_info 196 | 197 | def mypy( 198 | mypy_cli = None, 199 | mypy_ini = None, 200 | types = None, 201 | cache = True, 202 | color = True, 203 | suppression_tags = None, 204 | opt_in_tags = None): 205 | """ 206 | Create a mypy target inferring upstream caches from deps. 207 | 208 | Args: 209 | mypy_cli: (optional) a replacement mypy_cli to use (recommended to produce 210 | with mypy_cli macro) 211 | mypy_ini: (optional) mypy_ini file to use 212 | types: (optional) a dict of dependency label to types dependency label 213 | example: 214 | ``` 215 | { 216 | requirement("cachetools"): requirement("types-cachetools"), 217 | } 218 | ``` 219 | Use the types extension to create this map for a requirements.in 220 | or requirements.txt file. 221 | cache: (optional, default True) propagate the mypy cache 222 | color: (optional, default True) use color in mypy output 223 | suppression_tags: (optional, default ["no-mypy"]) tags that suppress running 224 | mypy on a particular target. 225 | opt_in_tags: (optional, default []) tags that must be present for mypy to run 226 | on a particular target. When specified, this ruleset will _only_ 227 | run on targets with this tag. 228 | 229 | Returns: 230 | a mypy aspect. 231 | """ 232 | types = types or {} 233 | 234 | additional_attrs = {} 235 | 236 | return aspect( 237 | implementation = _mypy_impl, 238 | attr_aspects = ["deps"], 239 | attrs = { 240 | "_mypy_cli": attr.label( 241 | default = mypy_cli or "@rules_mypy//mypy/private:mypy", 242 | cfg = "exec", 243 | executable = True, 244 | ), 245 | "_mypy_ini": attr.label( 246 | # we provide a default here because Bazel won't allow Label attrs 247 | # that are public, or private attrs that have default values of None 248 | default = mypy_ini or "@rules_mypy//mypy/private:default_mypy.ini", 249 | allow_single_file = True, 250 | mandatory = False, 251 | ), 252 | # pass the dict[Label, Label] in parts because Bazel doesn't have 253 | # this kind of attr to pass naturally 254 | "_types_keys": attr.label_list(default = types.keys()), 255 | "_types_values": attr.label_list(default = types.values()), 256 | "_suppression_tags": attr.string_list(default = suppression_tags or ["no-mypy"]), 257 | "_opt_in_tags": attr.string_list(default = opt_in_tags or []), 258 | "cache": attr.bool(default = cache), 259 | "color": attr.bool(default = color), 260 | } | additional_attrs, 261 | ) 262 | 263 | def mypy_cli(name, deps = None, mypy_requirement = None, python_version = "3.12", tags = None): 264 | """ 265 | Produce a custom mypy executable for use with the mypy build rule. 266 | 267 | Args: 268 | name: name of the binary target this macro produces 269 | deps: (optional) additional dependencies to include (e.g. mypy plugins) 270 | (note: must match the Python version of py_binary) 271 | mypy_requirement: (optional) a replacement mypy requirement 272 | (note: must match the Python version of py_binary) 273 | python_version: (optional) the python_version to use for this target. 274 | Pass None to use the default 275 | (defaults to a rules_mypy specified version, currently Python 3.12) 276 | tags: (optional) tags to include in the binary target 277 | """ 278 | 279 | deps = deps or [] 280 | mypy_requirement = mypy_requirement or requirement("mypy") 281 | 282 | py_binary( 283 | name = name, 284 | srcs = ["@rules_mypy//mypy/private:mypy_runner.py"], 285 | main = "@rules_mypy//mypy/private:mypy_runner.py", 286 | visibility = ["//visibility:public"], 287 | deps = [mypy_requirement] + deps, 288 | python_version = python_version, 289 | tags = tags, 290 | ) 291 | -------------------------------------------------------------------------------- /mypy/private/mypy_runner.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import contextlib 3 | import pathlib 4 | import os 5 | import shutil 6 | import sys 7 | import tempfile 8 | from typing import Any, Generator, Optional 9 | 10 | import mypy.api 11 | import mypy.util 12 | 13 | 14 | def _merge_upstream_caches(cache_dir: str, upstream_caches: list[str]) -> None: 15 | current = pathlib.Path(cache_dir) 16 | current.mkdir(parents=True, exist_ok=True) 17 | 18 | for upstream_dir in upstream_caches: 19 | upstream = pathlib.Path(upstream_dir) 20 | 21 | # TODO(mark): maybe there's a more efficient way to synchronize the cache dirs? 22 | for dirpath_str, _, filenames in os.walk(upstream.as_posix()): 23 | dirpath = pathlib.Path(dirpath_str) 24 | relative_dir = dirpath.relative_to(upstream) 25 | for file in filenames: 26 | upstream_path = dirpath / file 27 | target_path = current / relative_dir / file 28 | if not target_path.parent.exists(): 29 | target_path.parent.mkdir(parents=True) 30 | if not target_path.exists(): 31 | shutil.copy(upstream_path, target_path) 32 | 33 | # missing_stubs is mutable, so remove it 34 | missing_stubs = current / "missing_stubs" 35 | if missing_stubs.exists(): 36 | missing_stubs.unlink() 37 | 38 | 39 | @contextlib.contextmanager 40 | def managed_cache_dir( 41 | cache_dir: Optional[str], upstream_caches: list[str] 42 | ) -> Generator[str, Any, Any]: 43 | """ 44 | Returns a managed cache directory. 45 | 46 | When cache_dir exists, returns a merged view of cache_dir with upstream_caches. 47 | Otherwise, returns a temporary directory that will be cleaned up when the resource 48 | is released. 49 | """ 50 | if cache_dir: 51 | _merge_upstream_caches(cache_dir, list(upstream_caches)) 52 | yield cache_dir 53 | else: 54 | tmpdir = tempfile.TemporaryDirectory() 55 | yield tmpdir.name 56 | tmpdir.cleanup() 57 | 58 | 59 | def run_mypy( 60 | mypy_ini: Optional[str], cache_dir: str, srcs: list[str] 61 | ) -> tuple[str, str, int]: 62 | maybe_config = ["--config-file", mypy_ini] if mypy_ini else [] 63 | report, errors, status = mypy.api.run( 64 | maybe_config 65 | + [ 66 | # do not check mtime in cache 67 | "--skip-cache-mtime-checks", 68 | # mypy defaults to incremental, but force it on anyway 69 | "--incremental", 70 | # use a known cache-dir 71 | f"--cache-dir={cache_dir}", 72 | # use current dir + MYPYPATH to resolve deps 73 | "--explicit-package-bases", 74 | # speedup 75 | "--fast-module-lookup", 76 | ] 77 | + srcs 78 | ) 79 | if status: 80 | sys.stderr.write(errors) 81 | sys.stderr.write(report) 82 | 83 | return report, errors, status 84 | 85 | 86 | def run( 87 | output: Optional[str], 88 | cache_dir: Optional[str], 89 | upstream_caches: list[str], 90 | mypy_ini: Optional[str], 91 | srcs: list[str], 92 | ) -> None: 93 | if len(srcs) > 0: 94 | with managed_cache_dir(cache_dir, upstream_caches) as cache_dir: 95 | report, errors, status = run_mypy(mypy_ini, cache_dir, srcs) 96 | else: 97 | report, errors, status = "", "", 0 98 | 99 | if output: 100 | with open(output, "w+") as file: 101 | file.write(errors) 102 | file.write(report) 103 | 104 | # use mypy's hard_exit to exit without freeing objects, it can be meaningfully 105 | # faster than an orderly shutdown 106 | mypy.util.hard_exit(status) 107 | 108 | 109 | def main() -> None: 110 | parser = argparse.ArgumentParser() 111 | parser.add_argument("--output", required=False) 112 | parser.add_argument("-c", "--cache-dir", required=False) 113 | parser.add_argument("--upstream-cache", required=False, action="append") 114 | parser.add_argument("--mypy-ini", required=False) 115 | parser.add_argument("src", nargs="*") 116 | args = parser.parse_args() 117 | 118 | output: Optional[str] = args.output 119 | cache_dir: Optional[str] = args.cache_dir 120 | upstream_cache: list[str] = args.upstream_cache or [] 121 | mypy_ini: Optional[str] = args.mypy_ini 122 | srcs: list[str] = args.src 123 | 124 | run(output, cache_dir, upstream_cache, mypy_ini, srcs) 125 | 126 | 127 | if __name__ == "__main__": 128 | main() 129 | -------------------------------------------------------------------------------- /mypy/private/py_type_library.bzl: -------------------------------------------------------------------------------- 1 | "Convert pip typings packages for use with mypy." 2 | 3 | PyTypeLibraryInfo = provider( 4 | doc = "Information about the content of a py_type_library.", 5 | fields = { 6 | "directory": "Directory containing site-packages.", 7 | }, 8 | ) 9 | 10 | def _py_type_library_impl(ctx): 11 | directory = ctx.actions.declare_directory(ctx.attr.name) 12 | 13 | args = ctx.actions.args() 14 | args.add("--input-dir", ctx.attr.typing.label.workspace_root) 15 | args.add("--output-dir", directory.path) 16 | 17 | ctx.actions.run( 18 | mnemonic = "BuildPyTypeLibrary", 19 | inputs = depset(transitive = [ctx.attr.typing.default_runfiles.files]), 20 | outputs = [directory], 21 | executable = ctx.executable._exec, 22 | arguments = [args], 23 | env = ctx.configuration.default_shell_env, 24 | ) 25 | 26 | return [ 27 | DefaultInfo( 28 | files = depset([directory]), 29 | runfiles = ctx.runfiles(files = [directory]), 30 | ), 31 | PyTypeLibraryInfo( 32 | directory = directory, 33 | ), 34 | ] 35 | 36 | py_type_library = rule( 37 | implementation = _py_type_library_impl, 38 | attrs = { 39 | "typing": attr.label(), 40 | "_exec": attr.label(cfg = "exec", default = "//mypy/private:py_type_library", executable = True), 41 | }, 42 | ) 43 | -------------------------------------------------------------------------------- /mypy/private/py_type_library.py: -------------------------------------------------------------------------------- 1 | """ 2 | py_type_library CLI. 3 | 4 | Many of the typings/stubs published to pypi for package [x] show up use 5 | [x]-stubs as their base-package. mypy expects the typings/stubs to exist 6 | in the as-used-in-Python package [x] when placed on the path. This CLI 7 | creates a copy of the input directory's site-packages content dropping 8 | `-stubs` from any directory in the site-packages dir while copying. 9 | """ 10 | 11 | import pathlib 12 | import shutil 13 | 14 | import click 15 | 16 | 17 | def _clean(package: str) -> str: 18 | return package.removesuffix("-stubs") 19 | 20 | 21 | @click.command() 22 | @click.option("--input-dir", required=True, type=click.Path(exists=True)) 23 | @click.option("--output-dir", required=True, type=click.Path()) 24 | def main(input_dir: str, output_dir: str) -> None: 25 | input = pathlib.Path(input_dir) / "site-packages" 26 | 27 | output = pathlib.Path(output_dir) / "site-packages" 28 | output.mkdir(parents=True, exist_ok=True) 29 | 30 | for package in input.iterdir(): 31 | shutil.copytree(input / package.name, output / _clean(package.name)) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /mypy/private/requirements.in: -------------------------------------------------------------------------------- 1 | click~=8.2.1 2 | mypy~=1.16.0 3 | colorama~=0.4.6 4 | -------------------------------------------------------------------------------- /mypy/private/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # bazel run @@//mypy/private:generate_requirements_lock 3 | --index-url https://pypi.org/simple 4 | 5 | click==8.2.1 \ 6 | --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ 7 | --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b 8 | # via -r mypy/private/requirements.in 9 | colorama==0.4.6 \ 10 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 11 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 12 | # via -r mypy/private/requirements.in 13 | mypy==1.16.0 \ 14 | --hash=sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c \ 15 | --hash=sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff \ 16 | --hash=sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93 \ 17 | --hash=sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0 \ 18 | --hash=sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92 \ 19 | --hash=sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031 \ 20 | --hash=sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21 \ 21 | --hash=sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777 \ 22 | --hash=sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b \ 23 | --hash=sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb \ 24 | --hash=sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666 \ 25 | --hash=sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c \ 26 | --hash=sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2 \ 27 | --hash=sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491 \ 28 | --hash=sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab \ 29 | --hash=sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20 \ 30 | --hash=sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d \ 31 | --hash=sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b \ 32 | --hash=sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52 \ 33 | --hash=sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8 \ 34 | --hash=sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec \ 35 | --hash=sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13 \ 36 | --hash=sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1 \ 37 | --hash=sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571 \ 38 | --hash=sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730 \ 39 | --hash=sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2 \ 40 | --hash=sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090 \ 41 | --hash=sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436 \ 42 | --hash=sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3 \ 43 | --hash=sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b 44 | # via -r mypy/private/requirements.in 45 | mypy-extensions==1.0.0 \ 46 | --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ 47 | --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 48 | # via mypy 49 | pathspec==0.12.1 \ 50 | --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ 51 | --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 52 | # via mypy 53 | typing-extensions==4.12.2 \ 54 | --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ 55 | --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 56 | # via mypy 57 | -------------------------------------------------------------------------------- /mypy/private/types.bzl: -------------------------------------------------------------------------------- 1 | "Repository rule to generate `py_type_library` from input typings/stubs requirements." 2 | 3 | _PY_TYPE_LIBRARY_TEMPLATE = """ 4 | py_type_library( 5 | name = "{requirement}", 6 | typing = requirement("{requirement}"), 7 | visibility = ["//visibility:public"], 8 | ) 9 | """ 10 | 11 | def _render_build(rctx, types): 12 | content = "" 13 | content += """load("{pip_requirements}", "requirement")\n""".format( 14 | pip_requirements = rctx.attr.pip_requirements, 15 | ) 16 | content += """load("@rules_mypy//mypy:py_type_library.bzl", "py_type_library")\n""" 17 | for requirement in types: 18 | content += _PY_TYPE_LIBRARY_TEMPLATE.format( 19 | requirement = requirement, 20 | raw = requirement.removeprefix("types-").removesuffix("-stubs"), 21 | ) + "\n" 22 | return content 23 | 24 | def _render_types_bzl(rctx, types): 25 | content = "" 26 | content += """load("{pip_requirements}", "requirement")\n""".format( 27 | pip_requirements = rctx.attr.pip_requirements, 28 | ) 29 | content += "types = {\n" 30 | for requirement in types: 31 | content += """ requirement("{raw}"): "@@{name}//:{requirement}",\n""".format( 32 | raw = requirement.removeprefix("types-").removesuffix("-stubs"), 33 | name = str(rctx.attr.name), 34 | requirement = requirement, 35 | ) 36 | content += "}\n" 37 | return content 38 | 39 | def _generate_impl(rctx): 40 | contents = rctx.read(rctx.attr.requirements_txt) 41 | 42 | types = [] 43 | 44 | # this is a very, very naive parser 45 | for line in contents.splitlines(): 46 | if line.startswith("#") or line == "": 47 | continue 48 | 49 | if ";" in line: 50 | line, _ = line.split(";") 51 | 52 | if "~=" in line: 53 | req, _ = line.split("~=") 54 | elif "==" in line: 55 | req, _ = line.split("==") 56 | elif "<=" in line: 57 | req, _ = line.split("<=") 58 | else: 59 | continue 60 | 61 | req = req.strip() 62 | if req.endswith("-stubs") or req.startswith("types-"): 63 | types.append(req) 64 | 65 | rctx.file("BUILD.bazel", content = _render_build(rctx, types)) 66 | rctx.file("types.bzl", content = _render_types_bzl(rctx, types)) 67 | 68 | generate = repository_rule( 69 | implementation = _generate_impl, 70 | attrs = { 71 | "pip_requirements": attr.label(), 72 | "requirements_txt": attr.label(allow_single_file = True), 73 | }, 74 | ) 75 | -------------------------------------------------------------------------------- /mypy/py_type_library.bzl: -------------------------------------------------------------------------------- 1 | "Public API for py_type_library." 2 | 3 | load("//mypy/private:py_type_library.bzl", _py_type_library = "py_type_library") 4 | 5 | py_type_library = _py_type_library 6 | -------------------------------------------------------------------------------- /mypy/types.bzl: -------------------------------------------------------------------------------- 1 | "Extension for generating a types repository containing py_type_librarys for requirements." 2 | 3 | load("//mypy/private:types.bzl", "generate") 4 | 5 | requirements = tag_class( 6 | attrs = { 7 | "name": attr.string(), 8 | "pip_requirements": attr.label(), 9 | "requirements_txt": attr.label(mandatory = True, allow_single_file = True), 10 | }, 11 | ) 12 | 13 | def _extension(module_ctx): 14 | for mod in module_ctx.modules: 15 | for tag in mod.tags.requirements: 16 | generate( 17 | name = tag.name, 18 | pip_requirements = tag.pip_requirements, 19 | requirements_txt = tag.requirements_txt, 20 | ) 21 | 22 | types = module_extension( 23 | implementation = _extension, 24 | tag_classes = { 25 | "requirements": requirements, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rules_mypy 2 | 3 | An aspect to instrument `py_*` targets with mypy type-checking. 4 | 5 | Compared to [bazel-mypy-integration](https://github.com/bazel-contrib/bazel-mypy-integration), this ruleset aims to make a couple of improvements: 6 | 7 | - Propagation of the mypy cache between dependencies within a repository to avoid exponential type-checking work 8 | - Robust (and automated) support for including 3rd party types/stubs packages 9 | 10 | > [!WARNING] 11 | > rules_mypy's build actions produce mypy caches as outputs, and these may contain large file counts and that will only grow as a dependency chain grows. This may have an impact on the size and usage of build and/or remote caches. 12 | 13 | ## Usage 14 | 15 | This aspect will run over any `py_binary`, `py_library` or `py_test`. 16 | 17 | Setup is significantly easier with bzlmod, we recommend and predominantly support bzlmod, though these rules should work without issue in non-bzlmod setups, albeit with more work to configure. 18 | 19 | ### Bzlmod Setup 20 | 21 | **Add rules_mypy to your MODULE.bazel:** 22 | 23 | ```starlark 24 | bazel_dep(name = "rules_mypy", version = "0.0.0") 25 | ``` 26 | 27 | **Optionally, configure a types repository:** 28 | 29 | Many Python packages have separately published types/stubs packages. While mypy (and these rules) will work without including these types, this ruleset provides some utilities for leveraging these types to improve mypy's type checking. 30 | 31 | ```starlark 32 | types = use_extension("@rules_mypy//mypy:types.bzl", "types") 33 | types.requirements( 34 | name = "pip_types", 35 | # `@pip` in the next line corresponds to the `hub_name` when using 36 | # rules_python's `pip.parse(...)`. 37 | pip_requirements = "@pip//:requirements.bzl", 38 | # also legal to pass a `requirements.in` here 39 | requirements_txt = "//:requirements.txt", 40 | ) 41 | use_repo(types, "pip_types") 42 | ``` 43 | 44 | **Configure `mypy_aspect`.** 45 | 46 | Define a new aspect in a `.bzl` file (such as `./tools/aspects.bzl`): 47 | 48 | ```starlark 49 | load("@pip_types//:types.bzl", "types") 50 | load("@rules_mypy//mypy:mypy.bzl", "mypy") 51 | 52 | mypy_aspect = mypy(types = types) 53 | ``` 54 | 55 | Update your `.bazelrc` to include this new aspect: 56 | 57 | ```starlark 58 | # register mypy_aspect with Bazel 59 | build --aspects //tools:aspects.bzl%mypy_aspect 60 | 61 | # optionally, default enable the mypy checks 62 | build --output_groups=+mypy 63 | ``` 64 | 65 | ## Customizing mypy 66 | 67 | ### Configuring mypy with mypy.ini 68 | 69 | mypy's behavior may be customized using a [mypy config file](https://mypy.readthedocs.io/en/stable/config_file.html) file. To use a mypy config file, pass a label for a valid config file to the `mypy` aspect factory: 70 | 71 | ```starlark 72 | mypy_aspect = mypy( 73 | mypy_ini = "@@//:mypy.ini", 74 | types = types, 75 | ) 76 | ``` 77 | 78 | > [!NOTE] 79 | > The label passed to `mypy_ini` needs to be absolute (a prefix of `@@` means the root repo). 80 | 81 | > [!NOTE] 82 | > mypy.ini files should likely contain the following lines to suppress type-checking 3rd party modules. 83 | > 84 | > ``` 85 | > follow_imports = silent 86 | > follow_imports_for_stubs = True 87 | > ``` 88 | 89 | ### Changing the version of mypy and/or including plugins 90 | 91 | To customize the version of mypy, use rules_python's requirements resolution and construct a custom mypy CLI: 92 | 93 | ```starlark 94 | # in a BUILD file 95 | load("@pip//:requirements.bzl", "requirements") # '@pip' must match configured pip hub_name 96 | load("@rules_mypy//mypy:mypy.bzl", "mypy", "mypy_cli") 97 | 98 | mypy_cli( 99 | name = "mypy_cli", 100 | mypy_requirement = requirement("mypy"), 101 | ) 102 | ``` 103 | 104 | And in your `aspects.bzl` (or similar) file: 105 | 106 | ```starlark 107 | load("@rules_mypy//mypy:mypy.bzl", "mypy") 108 | 109 | mypy_aspect = mypy( 110 | mypy_cli = ":mypy_cli", 111 | types = types, 112 | ) 113 | ``` 114 | 115 | Further, to use mypy plugins referenced in any config file, use the `deps` attribute of `mypy_cli`: 116 | 117 | ```starlark 118 | # in a BUILD file 119 | load("@pip//:requirements.bzl", "requirement") # '@pip' must match configured pip hub_name 120 | load("@rules_mypy//mypy:mypy.bzl", "mypy", "mypy_cli") 121 | 122 | mypy_cli( 123 | name = "mypy_cli", 124 | mypy_requirement = requirement("mypy"), 125 | deps = [ 126 | requirement("pydantic"), 127 | ], 128 | ) 129 | ``` 130 | 131 | ## Skipping Targets 132 | 133 | Skip running mypy on targets by tagging with `no-mypy`, or customize the tags that will suppress mypy by providing a list to the `suppression_tags` argument of the mypy aspect initializer: 134 | 135 | ```starlark 136 | load("@rules_mypy//mypy:mypy.bzl", "mypy") 137 | 138 | mypy_aspect = mypy( 139 | suppression_tags = ["no-mypy", "no-checks"], 140 | types = types, 141 | ) 142 | ``` 143 | 144 | ## Running in opt-in mode 145 | 146 | To add type checking to a codebase incrementally, configure a list of opt-in tags that will suppress running mypy by default unless a target is tagged explicitly with one of the opt-in tags. 147 | 148 | ```starlark 149 | load("@rules_mypy//mypy:mypy.bzl", "mypy") 150 | 151 | mypy_aspect = mypy( 152 | opt_in_tags = ["typecheck"], 153 | types = types, 154 | ) 155 | ``` 156 | --------------------------------------------------------------------------------