├── .gitattributes ├── .github └── workflows │ ├── check.yaml │ ├── intellij-build.yml │ ├── intellij-release.yml │ └── intellij-run-ui-tests.yml ├── .gitignore ├── .idea ├── .gitignore ├── basedtyping.iml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── externalDependencies.xml ├── git_toolbox_prj.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── pylint.xml └── vcs.xml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── basedtyping ├── __init__.py ├── py.typed ├── runtime_only.py ├── transformer.py └── typetime_only.py ├── intellij ├── .gitignore ├── .run │ ├── Run Plugin.run.xml │ ├── Run Tests.run.xml │ └── Run Verifications.run.xml ├── CHANGELOG.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle │ ├── libs.versions.toml │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── org │ │ │ └── basedsoft │ │ │ └── plugins │ │ │ └── basedtyping │ │ │ └── BasedTypingTypeProvider.kt │ └── resources │ │ └── META-INF │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ └── test │ ├── java │ └── com │ │ └── jetbrains │ │ └── python │ │ ├── PythonMockSdk.java │ │ ├── README.md │ │ └── fixtures │ │ ├── PyLightProjectDescriptor.java │ │ └── PyTestCase.java │ └── kotlin │ └── org │ └── basedsoft │ └── plugins │ └── basedtyping │ └── TestBasedTypeProvider.kt ├── poetry.lock ├── pw ├── pw.bat ├── pyproject.toml └── tests ├── __init__.py ├── test_as_functiontype.py ├── test_basedmypy_typechecking.py ├── test_basedspecialform.py ├── test_function_type.py ├── test_get_type_hints.py ├── test_intersection.py ├── test_is_subform.py ├── test_never_type ├── __init__.py ├── test_runtime.py └── test_typetime.py ├── test_reified_generics ├── __init__.py ├── test_identity.py ├── test_isinstance.py ├── test_issubclass.py ├── test_not_enough_type_params.py ├── test_reified_generic.py ├── test_subclasses.py ├── test_type_of_types.py ├── test_typevars.py └── test_variance.py ├── test_runtime_only ├── __init__.py ├── test_literal_type.py └── test_old_union_type.py ├── test_transformer.py ├── test_typeform.py └── test_typetime_only ├── __init__.py ├── test_assert_type.py └── test_typetime_only.py /.gitattributes: -------------------------------------------------------------------------------- 1 | intellij/src/test/java/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: push 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | include: 11 | - python-version: "3.9" 12 | usable-python-version: "3.9" 13 | - python-version: "3.10" 14 | usable-python-version: "3.10" 15 | - python-version: "3.11" 16 | usable-python-version: "3.11" 17 | - python-version: "3.12" 18 | usable-python-version: "3.12" 19 | - python-version: "3.13" 20 | usable-python-version: "3.13" 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - run: ./pw poetry config virtualenvs.in-project true 28 | - name: Set up cache 29 | uses: actions/cache@v4 30 | id: cache 31 | with: 32 | path: .venv 33 | key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 34 | - name: Ensure cache is healthy 35 | if: steps.cache.outputs.cache-hit == 'true' 36 | run: timeout 10s poetry run pip --version || rm -rf .venv 37 | - run: ./pw poetry install 38 | - run: ./pw poetry run mypy -p basedtyping -p tests --python-version ${{ matrix.usable-python-version }} 39 | - run: ./pw poetry run pytest tests/ 40 | 41 | lint: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: actions/setup-python@v5 46 | with: 47 | python-version: "3.9" 48 | - run: ./pw poetry config virtualenvs.in-project true 49 | - name: Set up cache 50 | uses: actions/cache@v4 51 | id: cache 52 | with: 53 | path: .venv 54 | key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} 55 | - name: Ensure cache is healthy 56 | if: steps.cache.outputs.cache-hit == 'true' 57 | run: timeout 10s ./pw poetry run pip --version || rm -rf .venv 58 | - run: ./pw poetry install 59 | - run: ./pw poetry run ruff format --check --diff 60 | - run: ./pw poetry run ruff check 61 | -------------------------------------------------------------------------------- /.github/workflows/intellij-build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run Qodana inspections. 5 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 6 | # - Run the 'runPluginVerifier' task. 7 | # - Create a draft release. 8 | # 9 | # The workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | ## JBIJPPTPL 14 | 15 | name: Build 16 | on: 17 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 18 | push: 19 | branches: [ main ] 20 | # Trigger the workflow on any pull request 21 | pull_request: 22 | paths: 23 | - "intellij/**" 24 | 25 | concurrency: 26 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | 31 | # Prepare environment and build the plugin 32 | build: 33 | name: Build 34 | runs-on: ubuntu-latest 35 | outputs: 36 | version: ${{ steps.properties.outputs.version }} 37 | changelog: ${{ steps.properties.outputs.changelog }} 38 | pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} 39 | steps: 40 | 41 | # Check out the current repository 42 | - name: Fetch Sources 43 | uses: actions/checkout@v4 44 | 45 | # Validate wrapper 46 | - name: Gradle Wrapper Validation 47 | uses: gradle/actions/wrapper-validation@v3 48 | 49 | # Set up Java environment for the next steps 50 | - name: Setup Java 51 | uses: actions/setup-java@v4 52 | with: 53 | distribution: zulu 54 | java-version: 17 55 | 56 | # Setup Gradle 57 | - name: Setup Gradle 58 | uses: gradle/actions/setup-gradle@v4 59 | 60 | # Set environment variables 61 | - name: Export Properties 62 | working-directory: ./intellij 63 | id: properties 64 | shell: bash 65 | run: | 66 | PROPERTIES="$(./gradlew properties --console=plain -q)" 67 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 68 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 69 | 70 | echo "version=$VERSION" >> $GITHUB_OUTPUT 71 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 72 | 73 | echo "changelog<> $GITHUB_OUTPUT 74 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 75 | echo "EOF" >> $GITHUB_OUTPUT 76 | 77 | # Build plugin 78 | - name: Build plugin 79 | working-directory: ./intellij 80 | run: ./gradlew buildPlugin 81 | 82 | # Prepare plugin archive content for creating artifact 83 | - name: Prepare Plugin Artifact 84 | id: artifact 85 | shell: bash 86 | run: | 87 | cd ${{ github.workspace }}/intellij/build/distributions 88 | FILENAME=`ls *.zip` 89 | unzip "$FILENAME" -d content 90 | 91 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 92 | 93 | # Store already-built plugin as an artifact for downloading 94 | - name: Upload artifact 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: ${{ steps.artifact.outputs.filename }} 98 | path: ./intellij/build/distributions/content/*/* 99 | 100 | # Run tests and upload a code coverage report 101 | test: 102 | name: Test 103 | needs: [ build ] 104 | runs-on: ubuntu-latest 105 | steps: 106 | 107 | # Check out the current repository 108 | - name: Fetch Sources 109 | uses: actions/checkout@v4 110 | 111 | # Set up Java environment for the next steps 112 | - name: Setup Java 113 | uses: actions/setup-java@v4 114 | with: 115 | distribution: zulu 116 | java-version: 17 117 | 118 | # Setup Gradle 119 | - name: Setup Gradle 120 | uses: gradle/actions/setup-gradle@v4 121 | 122 | # Run tests 123 | - name: Run Tests 124 | working-directory: ./intellij 125 | run: ./gradlew check 126 | 127 | # Collect Tests Result of failed tests 128 | - name: Collect Tests Result 129 | if: ${{ failure() }} 130 | uses: actions/upload-artifact@v4 131 | with: 132 | name: tests-result 133 | path: ${{ github.workspace }}/intellij/build/reports/tests 134 | 135 | # Upload the Kover report to CodeCov 136 | - name: Upload Code Coverage Report 137 | uses: codecov/codecov-action@v4 138 | with: 139 | files: ${{ github.workspace }}/intellij/build/reports/kover/report.xml 140 | 141 | # Run Qodana inspections and provide report 142 | inspectCode: 143 | name: Inspect code 144 | needs: [ build ] 145 | runs-on: ubuntu-latest 146 | permissions: 147 | contents: write 148 | checks: write 149 | pull-requests: write 150 | steps: 151 | 152 | # Free GitHub Actions Environment Disk Space 153 | - name: Maximize Build Space 154 | uses: jlumbroso/free-disk-space@main 155 | with: 156 | tool-cache: false 157 | large-packages: false 158 | 159 | # Check out the current repository 160 | - name: Fetch Sources 161 | uses: actions/checkout@v4 162 | with: 163 | ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit 164 | fetch-depth: 0 # a full history is required for pull request analysis 165 | 166 | # Set up Java environment for the next steps 167 | - name: Setup Java 168 | uses: actions/setup-java@v4 169 | with: 170 | distribution: zulu 171 | java-version: 17 172 | 173 | # Run Qodana inspections 174 | # - name: Qodana - Code Inspection 175 | # uses: JetBrains/qodana-action@v2024.1.5 176 | # with: 177 | # cache-default-branch-only: true 178 | 179 | # Run plugin structure verification along with IntelliJ Plugin Verifier 180 | verify: 181 | name: Verify plugin 182 | needs: [ build ] 183 | runs-on: ubuntu-latest 184 | steps: 185 | 186 | # Free GitHub Actions Environment Disk Space 187 | - name: Maximize Build Space 188 | uses: jlumbroso/free-disk-space@main 189 | with: 190 | tool-cache: false 191 | large-packages: false 192 | 193 | # Check out the current repository 194 | - name: Fetch Sources 195 | uses: actions/checkout@v4 196 | 197 | # Set up Java environment for the next steps 198 | - name: Setup Java 199 | uses: actions/setup-java@v4 200 | with: 201 | distribution: zulu 202 | java-version: 17 203 | 204 | # Setup Gradle 205 | - name: Setup Gradle 206 | uses: gradle/actions/setup-gradle@v4 207 | 208 | # Cache Plugin Verifier IDEs 209 | - name: Setup Plugin Verifier IDEs Cache 210 | uses: actions/cache@v4 211 | with: 212 | path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides 213 | key: plugin-verifier-${{ hashFiles('intellij/build/listProductsReleases.txt') }} 214 | 215 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 216 | - name: Run Plugin Verification tasks 217 | working-directory: ./intellij 218 | run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} 219 | 220 | # Collect Plugin Verifier Result 221 | - name: Collect Plugin Verifier Result 222 | if: ${{ always() }} 223 | uses: actions/upload-artifact@v4 224 | with: 225 | name: pluginVerifier-result 226 | path: ${{ github.workspace }}/intellij/build/reports/pluginVerifier 227 | 228 | # Prepare a draft release for GitHub Releases page for the manual verification 229 | # If accepted and published, release workflow would be triggered 230 | # releaseDraft: 231 | # name: Release draft 232 | # if: github.event_name != 'pull_request' 233 | # needs: [ build, test, inspectCode, verify ] 234 | # runs-on: ubuntu-latest 235 | # permissions: 236 | # contents: write 237 | # steps: 238 | # 239 | # # Check out the current repository 240 | # - name: Fetch Sources 241 | # uses: actions/checkout@v4 242 | # 243 | # # Remove old release drafts by using the curl request for the available releases with a draft flag 244 | # - name: Remove Old Release Drafts 245 | # env: 246 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 247 | # run: | 248 | # gh api repos/{owner}/{repo}/releases \ 249 | # --jq '.[] | select(.draft == true) | .id' \ 250 | # | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 251 | # 252 | # # Create a new release draft which is not publicly visible and requires manual acceptance 253 | # - name: Create Release Draft 254 | # env: 255 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 256 | # run: | 257 | # gh release create "v${{ needs.build.outputs.version }}" \ 258 | # --draft \ 259 | # --title "v${{ needs.build.outputs.version }}" \ 260 | # --notes "$(cat << 'EOM' 261 | # ${{ needs.build.outputs.changelog }} 262 | # EOM 263 | # )" 264 | -------------------------------------------------------------------------------- /.github/workflows/intellij-release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | workflow_dispatch: 8 | # release: 9 | # types: [prereleased, released] 10 | 11 | jobs: 12 | 13 | # Prepare and publish the plugin to JetBrains Marketplace repository 14 | release: 15 | name: Publish Plugin 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | steps: 21 | 22 | # Check out the current repository 23 | - name: Fetch Sources 24 | uses: actions/checkout@v4 25 | with: 26 | ref: ${{ github.event.release.tag_name }} 27 | 28 | # Set up Java environment for the next steps 29 | - name: Setup Java 30 | uses: actions/setup-java@v4 31 | with: 32 | distribution: zulu 33 | java-version: 17 34 | 35 | # Setup Gradle 36 | - name: Setup Gradle 37 | uses: gradle/actions/setup-gradle@v4 38 | 39 | # Set environment variables 40 | - name: Export Properties 41 | id: properties 42 | shell: bash 43 | run: | 44 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 45 | ${{ github.event.release.body }} 46 | EOM 47 | )" 48 | 49 | echo "changelog<> $GITHUB_OUTPUT 50 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 51 | echo "EOF" >> $GITHUB_OUTPUT 52 | 53 | # Update the Unreleased section with the current release note 54 | - name: Patch Changelog 55 | working-directory: ./intellij 56 | if: ${{ steps.properties.outputs.changelog != '' }} 57 | env: 58 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 59 | run: | 60 | ./gradlew patchChangelog --release-note="$CHANGELOG" 61 | 62 | # Publish the plugin to JetBrains Marketplace 63 | - name: Publish Plugin 64 | working-directory: ./intellij 65 | env: 66 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 67 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 68 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 69 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 70 | run: ./gradlew publishPlugin 71 | 72 | # Upload artifact as a release asset 73 | - name: Upload Release Asset 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: gh release upload ${{ github.event.release.tag_name }} ./intellij/build/distributions/* 77 | 78 | # Create a pull request 79 | # - name: Create Pull Request 80 | # if: ${{ steps.properties.outputs.changelog != '' }} 81 | # env: 82 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | # run: | 84 | # VERSION="${{ github.event.release.tag_name }}" 85 | # BRANCH="changelog-update-$VERSION" 86 | # LABEL="release changelog" 87 | # 88 | # git config user.email "action@github.com" 89 | # git config user.name "GitHub Action" 90 | # 91 | # git checkout -b $BRANCH 92 | # git commit -am "Changelog update - $VERSION" 93 | # git push --set-upstream origin $BRANCH 94 | # 95 | # gh label create "$LABEL" \ 96 | # --description "Pull requests with release changelog update" \ 97 | # --force \ 98 | # || true 99 | # 100 | # gh pr create \ 101 | # --title "Changelog update - \`$VERSION\`" \ 102 | # --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 103 | # --label "$LABEL" \ 104 | # --head $BRANCH 105 | -------------------------------------------------------------------------------- /.github/workflows/intellij-run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out the current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v4 37 | 38 | # Set up Java environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: zulu 43 | java-version: 17 44 | 45 | # Setup Gradle 46 | - name: Setup Gradle 47 | uses: gradle/actions/setup-gradle@v4 48 | 49 | # Run IDEA prepared for UI testing 50 | - name: Run IDE 51 | run: ${{ matrix.runIde }} 52 | 53 | # Wait for IDEA to be started 54 | - name: Health Check 55 | uses: jtalk/url-health-check-action@v4 56 | with: 57 | url: http://127.0.0.1:8082 58 | max-attempts: 15 59 | retry-delay: 30s 60 | 61 | # Run tests 62 | - name: Tests 63 | run: ./gradlew test 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | /.venv/ 3 | /venv/ 4 | /dist/ 5 | /.pyprojectx 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | 10 | misc.xml 11 | -------------------------------------------------------------------------------- /.idea/basedtyping.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 24 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/externalDependencies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/pylint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.vscode-pylance", 5 | "charliermarsh.ruff", 6 | "eamodio.gitlens", 7 | "mhutchie.git-graph", 8 | "ninoseki.vscode-pylens", 9 | "tamasfe.even-better-toml", 10 | "redhat.vscode-yaml" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "current file justMyCode disabled", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "git.allowForcePush": true, 4 | "git.autofetch": "all", 5 | "git.autoStash": true, 6 | "git.pruneOnFetch": true, 7 | "git.rebaseWhenSync": true, 8 | "git.suggestSmartCommit": false, 9 | "git.supportCancellation": true, 10 | "git.useEditorAsCommitInput": false, 11 | "diffEditor.ignoreTrimWhitespace": false, 12 | "python.linting.mypyArgs": [ 13 | "--show-column-numbers", 14 | "--no-pretty", 15 | "--hide-error-context" 16 | ], 17 | "python.linting.mypyEnabled": true, 18 | "python.linting.enabled": true, 19 | "files.autoSave": "onFocusChange", 20 | "search.useIgnoreFiles": true, 21 | "git.useCommitInputAsStashMessage": true, 22 | "editor.codeActionsOnSave": { 23 | "source.fixAll": true, 24 | "source.organizeImports": true 25 | }, 26 | "files.eol": "\n", 27 | "python.testing.pytestEnabled": true, 28 | "python.analysis.typeCheckingMode": "off", 29 | "yaml.schemas": { 30 | "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json": ".github/workflows/*.yaml" 31 | }, 32 | "[python]": { 33 | "editor.defaultFormatter": "charliermarsh.ruff" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "poetry install requirements", 8 | "type": "shell", 9 | "command": "poetry", 10 | "args": ["install"], 11 | "presentation": { 12 | "clear": true 13 | }, 14 | "problemMatcher": [] 15 | }, 16 | { 17 | "label": "poetry update lockfile", 18 | "type": "shell", 19 | "command": "poetry", 20 | "args": ["update"], 21 | "presentation": { 22 | "clear": true 23 | }, 24 | "problemMatcher": [] 25 | }, 26 | { 27 | "label": "poetry refresh lockfile (no update)", 28 | "type": "shell", 29 | "command": "poetry", 30 | "args": ["lock", "--no-update"], 31 | "presentation": { 32 | "clear": true 33 | }, 34 | "problemMatcher": [] 35 | }, 36 | { 37 | "label": "basedmypy - all files", 38 | "type": "shell", 39 | "command": "${command:python.interpreterPath}", 40 | "args": ["-m", "mypy", "-p", "basedtyping", "-p", "tests"], 41 | "presentation": { 42 | "clear": true 43 | }, 44 | "problemMatcher": [] 45 | }, 46 | { 47 | "label": "ruff format - all files", 48 | "type": "shell", 49 | "command": "${command:python.interpreterPath}", 50 | "args": ["-m", "ruff", "format", "."], 51 | "presentation": { 52 | "clear": true 53 | }, 54 | "problemMatcher": [] 55 | }, { 56 | "label": "ruff - all files", 57 | "type": "shell", 58 | "command": "${command:python.interpreterPath}", 59 | "args": ["-m", "ruff", "."], 60 | "presentation": { 61 | "clear": true 62 | }, 63 | "problemMatcher": [] 64 | }, 65 | { 66 | "label": "ruff fix - all files", 67 | "type": "shell", 68 | "command": "${command:python.interpreterPath}", 69 | "args": ["-m", "ruff", "--fix", "."], 70 | "presentation": { 71 | "clear": true 72 | }, 73 | "problemMatcher": [] 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 KotlinIsland 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 | # basedtyping 2 | 3 | A collection of helpers and utilities to aid at dealing with types, both at static analysis and at runtime. 4 | 5 | It's recommended to use [`basedmypy`](https://github.com/kotlinisland/basedmypy) when using `basedtyping`, 6 | as there are specialised adaptations made to `basedmypy` to support some functionality of this package. 7 | 8 | 9 | ## Features 10 | ### `ReifiedGeneric` 11 | A ``Generic`` where the type parameters are available at runtime and usable in ``isinstance`` and ``issubclass`` checks. 12 | 13 | For example: 14 | ```py 15 | class Foo(ReifiedGeneric[T]): 16 | def hi(self): 17 | print("Hi :)") 18 | 19 | def foo(it: object): 20 | # no error, as the class is reified and can be checked at runtime 21 | if isinstance(it, Foo[int]): 22 | print("wooow 😳") 23 | ``` 24 | 25 | ### `assert_type` 26 | A type-time function used for testing types: 27 | ```py 28 | from typing import TYPE_CHECKING 29 | 30 | if TYPE_CHECKING: 31 | assert_type[int](foo) # type error if `foo` isn't an `int` 32 | ``` 33 | 34 | ### And many more! 35 | -------------------------------------------------------------------------------- /basedtyping/__init__.py: -------------------------------------------------------------------------------- 1 | """The main ``basedtyping`` module. the types/functions defined here can be used at 2 | both type-time and at runtime. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import ast 8 | import sys 9 | import types 10 | import typing 11 | import warnings 12 | from typing import ( # type: ignore[attr-defined] 13 | TYPE_CHECKING, 14 | Any, 15 | Callable, 16 | Final, 17 | Generic, 18 | Mapping, 19 | NoReturn, 20 | Sequence, 21 | Tuple, 22 | Type, 23 | TypeVar, 24 | Union, 25 | _GenericAlias, 26 | _remove_dups_flatten, 27 | _SpecialForm, 28 | _tp_cache, 29 | cast, 30 | ) 31 | 32 | import typing_extensions 33 | from typing_extensions import Never, ParamSpec, Self, TypeAlias, TypeGuard, TypeVarTuple, override 34 | 35 | from basedtyping import transformer 36 | from basedtyping.runtime_only import OldUnionType 37 | 38 | # TODO: `Final[Literal[False]]` basedmypy will still whinge on usages 39 | # https://github.com/KotlinIsland/basedmypy/issues/782 40 | BASEDMYPY_TYPE_CHECKING: Final = False 41 | """special constants, are always `False`, but will always assume it to be true 42 | by the respective tool 43 | """ 44 | 45 | 46 | if not TYPE_CHECKING: 47 | if sys.version_info >= (3, 13): 48 | from typing import _collect_type_parameters as _collect_parameters 49 | elif sys.version_info >= (3, 11): 50 | from typing import _collect_parameters 51 | else: 52 | from typing import _collect_type_vars as _collect_parameters 53 | 54 | __all__ = ( 55 | "AnyCallable", 56 | "FunctionType", 57 | "TCallable", 58 | "TFunction", 59 | "Function", 60 | "T", 61 | "in_T", 62 | "out_T", 63 | "Ts", 64 | "P", 65 | "Fn", 66 | "ReifiedGenericError", 67 | "NotReifiedError", 68 | "ReifiedGeneric", 69 | "NotEnoughTypeParametersError", 70 | "issubform", 71 | "Untyped", 72 | "Intersection", 73 | "TypeForm", 74 | "as_functiontype", 75 | "ForwardRef", 76 | "BASEDMYPY_TYPE_CHECKING", 77 | "get_type_hints", 78 | ) 79 | 80 | if TYPE_CHECKING: 81 | _tp_cache_typed: Callable[[T], T] 82 | else: 83 | _tp_cache_typed = _tp_cache 84 | 85 | 86 | class _BasedSpecialForm(_SpecialForm, _root=True): # type: ignore[misc] 87 | _name: str 88 | 89 | @override 90 | def __init_subclass__(cls, _root=False): 91 | super().__init_subclass__(_root=_root) # type: ignore[call-arg] 92 | 93 | def __init__(self, *args: object, **kwargs: object): 94 | self.alias = kwargs.pop("alias", _BasedGenericAlias) 95 | super().__init__(*args, **kwargs) 96 | 97 | @override 98 | def __repr__(self) -> str: 99 | return "basedtyping." + self._name 100 | 101 | def __and__(self, other: object) -> object: 102 | return Intersection[self, other] 103 | 104 | def __rand__(self, other: object) -> object: 105 | return Intersection[other, self] 106 | 107 | 108 | class _BasedGenericAlias(_GenericAlias, _root=True): 109 | def __and__(self, other: object) -> object: 110 | return Intersection[self, other] 111 | 112 | def __rand__(self, other: object) -> object: 113 | return Intersection[other, self] 114 | 115 | 116 | if TYPE_CHECKING: 117 | Function = Callable[..., object] # type: ignore[no-any-explicit] 118 | """deprecated, use `AnyCallable`/`AnyFunction` instead. Any ``Callable``. 119 | 120 | useful when using mypy with ``disallow-any-explicit`` 121 | due to https://github.com/python/mypy/issues/9496 122 | 123 | Cannot actually be called unless it's narrowed, so it should only really be used as 124 | a bound in a ``TypeVar``. 125 | """ 126 | else: 127 | # for isinstance checks 128 | Function = Callable 129 | 130 | # Unlike the generics in other modules, these are meant to be imported to save you 131 | # from the boilerplate 132 | T = TypeVar("T") 133 | in_T = TypeVar("in_T", contravariant=True) 134 | out_T = TypeVar("out_T", covariant=True) 135 | Ts = TypeVarTuple("Ts") 136 | P = ParamSpec("P") 137 | 138 | AnyCallable = Callable[..., object] # type: ignore[no-any-explicit] 139 | """Any ``Callable``. useful when using mypy with ``disallow-any-explicit`` 140 | due to https://github.com/python/mypy/issues/9496 141 | 142 | Cannot actually be called unless it's narrowed, so it should only really be used as 143 | a bound in a ``TypeVar``. 144 | """ 145 | 146 | 147 | if not BASEDMYPY_TYPE_CHECKING and TYPE_CHECKING: 148 | FunctionType: TypeAlias = Callable[P, T] 149 | else: 150 | # TODO: BasedSpecialGenericAlias # noqa: TD003 151 | FunctionType: _SpecialForm = typing._CallableType(types.FunctionType, 2) # type: ignore[attr-defined] 152 | 153 | AnyFunction = FunctionType[..., object] # type: ignore[no-any-explicit] 154 | 155 | TCallable = TypeVar("TCallable", bound=AnyCallable) 156 | TFunction = TypeVar("TFunction", bound=AnyFunction) 157 | Fn = TypeVar("Fn", bound=AnyCallable) 158 | """deprecated, use `TCallable` or `TFunction` instead""" 159 | 160 | 161 | def _type_convert(arg: object) -> object: 162 | """For converting None to type(None), and strings to ForwardRef. 163 | 164 | Stolen from typing. 165 | """ 166 | if arg is None: 167 | return type(None) 168 | if isinstance(arg, str): 169 | return ForwardRef(arg) 170 | return arg 171 | 172 | 173 | class ReifiedGenericError(TypeError): 174 | pass 175 | 176 | 177 | class NotReifiedError(ReifiedGenericError): 178 | """Raised when a ``ReifiedGeneric`` is instantiated without passing type parameters: 179 | 180 | ie: ``foo: Foo[int] = Foo()`` instead of ``foo = Foo[int]()`` 181 | 182 | or when a ``ReifiedGeneric`` is instantiated with a non-reified ``TypeVar`` 183 | as a type parameter instead of a concrete type. 184 | 185 | ie: ``Foo[T]()`` instead of ``Foo[int]()`` 186 | """ 187 | 188 | 189 | class NotEnoughTypeParametersError(ReifiedGenericError): 190 | """Raised when type parameters are passed to a ``ReifiedGeneric`` with an 191 | incorrect number of type parameters: 192 | 193 | for example: 194 | >>> class Foo(ReifiedGeneric[Tuple[T, U]]): 195 | ... ... 196 | ... 197 | ... foo = Foo[int]() # wrong 198 | ... foo = Foo[int, str]() # correct 199 | """ 200 | 201 | 202 | class _ReifiedGenericMetaclass(type): 203 | # these should really only be on the class not the metaclass, 204 | # but since it needs to be accessible from both instances and the class itself, 205 | # its duplicated here 206 | 207 | __reified_generics__: tuple[type, ...] 208 | """should be a generic but cant due to https://github.com/python/mypy/issues/11672""" 209 | 210 | __type_vars__: tuple[TypeVar, ...] 211 | """``TypeVar``s that have not yet been reified. so this Tuple should always be empty 212 | by the time the ``ReifiedGeneric`` is instanciated""" 213 | 214 | _orig_type_vars: tuple[TypeVar, ...] 215 | """used internally to check the ``__type_vars__`` on the current ``ReifiedGeneric`` 216 | against the original one it was copied from 217 | in ``ReifiedGeneric.__class_getitem__``""" 218 | 219 | _can_do_instance_and_subclass_checks_without_generics: bool 220 | """Used internally for ``isinstance`` and ``issubclass`` checks, ``True`` 221 | when the class can currenty be used in said checks without generics in them""" 222 | 223 | def _orig_class(cls) -> _ReifiedGenericMetaclass: 224 | """Gets the original class that ``ReifiedGeneric.__class_getitem__`` copied from""" 225 | result = cls.__bases__[0] 226 | if result is ReifiedGeneric: 227 | return cls 228 | return result # type: ignore[return-value] 229 | 230 | def _type_var_check(cls, args: tuple[type, ...]) -> bool: 231 | if not cls._generics_are_reified(): 232 | if cls._has_non_reified_type_vars(): 233 | cls._raise_generics_not_reified() 234 | return True 235 | if len(cls._orig_class().__parameters__) != len(cls.__reified_generics__) == len(args): # type: ignore[attr-defined] 236 | raise RuntimeError 237 | for parameter, self_arg, subclass_arg in zip( 238 | # normal generics use __parameters__, we use __type_vars__ because the 239 | # Generic base class deletes properties named __parameters__ when copying 240 | # to a new class 241 | cast( 242 | Tuple[TypeVar, ...], 243 | cls._orig_class().__parameters__, # type: ignore[attr-defined] 244 | ), 245 | cls.__reified_generics__, 246 | args, 247 | ): 248 | if parameter.__contravariant__: 249 | if not issubform(self_arg, subclass_arg): 250 | return False 251 | elif parameter.__covariant__: 252 | if not issubform(subclass_arg, self_arg): 253 | return False 254 | elif subclass_arg != self_arg: 255 | return False 256 | return True 257 | 258 | def _generics_are_reified(cls) -> bool: 259 | return hasattr(cls, "__type_vars__") and not bool(cls.__type_vars__) 260 | 261 | def _has_non_reified_type_vars(cls) -> bool: 262 | return hasattr(cls, "__type_vars__") and bool(cls.__type_vars__) 263 | 264 | def _raise_generics_not_reified(cls) -> NoReturn: 265 | raise NotReifiedError( 266 | f"Type {cls.__name__} cannot be instantiated; TypeVars cannot be used" 267 | f" to instantiate a reified class: {cls._orig_type_vars}" 268 | ) 269 | 270 | def _check_generics_reified(cls): 271 | if not cls._generics_are_reified() or cls._has_non_reified_type_vars(): 272 | cls._raise_generics_not_reified() 273 | 274 | def _is_subclass(cls, subclass: object) -> TypeGuard[_ReifiedGenericMetaclass]: 275 | """For ``__instancecheck__`` and ``__subclasscheck__``. checks whether the 276 | "origin" type (ie. without the generics) is a subclass of this reified generic 277 | """ 278 | # could be any random instance, check it's a reified generic first: 279 | return type.__instancecheck__( 280 | _ReifiedGenericMetaclass, 281 | subclass, 282 | # then check that the instance is an instance of this particular reified generic: 283 | ) and type.__subclasscheck__( 284 | cls._orig_class(), 285 | # https://github.com/python/mypy/issues/11671 286 | cast(_ReifiedGenericMetaclass, subclass)._orig_class(), 287 | ) 288 | 289 | @override 290 | def __subclasscheck__(cls, subclass: object) -> bool: 291 | if not cls._is_subclass(subclass): 292 | return False 293 | if cls._can_do_instance_and_subclass_checks_without_generics: 294 | return True 295 | # if one of the classes doesn't have any generics, we treat it as the widest 296 | # possible values for those generics (like star projection) 297 | if not hasattr(subclass, "__reified_generics__"): 298 | # TODO: subclass could be wider, but we don't know for sure because cls could have generics matching its bound # noqa: TD003 299 | raise NotImplementedError( 300 | "Cannot perform a subclass check where the first class" 301 | f" ({cls.__name__!r}) has type parameters and the second class" 302 | f" ({subclass.__name__!r}) doesn't" 303 | ) 304 | if not hasattr(cls, "__reified_generics__"): 305 | # subclass would be narrower, so we can safely return True 306 | return True 307 | subclass._check_generics_reified() 308 | return cls._type_var_check(subclass.__reified_generics__) 309 | 310 | @override 311 | def __instancecheck__(cls, instance: object) -> bool: 312 | if not cls._is_subclass(type(instance)): 313 | return False 314 | if cls._can_do_instance_and_subclass_checks_without_generics: 315 | return True 316 | return cls._type_var_check(cast(ReifiedGeneric[object], instance).__reified_generics__) 317 | 318 | # need the generic here for pyright. see https://github.com/microsoft/pyright/issues/5488 319 | @override 320 | def __call__(cls: type[T], *args: object, **kwargs: object) -> T: 321 | """A placeholder ``__call__`` method that gets called when the class is 322 | instantiated directly, instead of first supplying the type parameters. 323 | """ 324 | cls_narrowed = cast(Type[ReifiedGeneric[object]], cls) 325 | if ( 326 | # instantiating a ReifiedGeneric without specifying any TypeVars 327 | not hasattr(cls_narrowed, "_orig_type_vars") 328 | # instantiating a subtype of a ReifiedGeneric without specifying any TypeVars 329 | or cls_narrowed._orig_type_vars == cls_narrowed.__type_vars__ 330 | ): 331 | raise NotReifiedError( 332 | f"Cannot instantiate ReifiedGeneric {cls_narrowed.__name__!r} because" 333 | " its type parameters were not supplied. The type parameters must be" 334 | " explicitly specified in the instantiation so that the type data can" 335 | " be made available at runtime.\n\nFor example:\n\nfoo: Foo[int] =" 336 | " Foo() #wrong\nfoo = Foo[T]() #wrong\nfoo = Foo[int]() # correct" 337 | ) 338 | cls_narrowed._check_generics_reified() 339 | # see comment about cls above 340 | return cast(T, super().__call__(*args, **kwargs)) # type:ignore[misc] 341 | 342 | 343 | GenericItems: TypeAlias = Union[type, TypeVar, Tuple[Union[type, TypeVar], ...]] 344 | """The ``items`` argument passed to ``__class_getitem__`` when creating or using a ``Generic``""" 345 | 346 | 347 | class ReifiedGeneric(Generic[T], metaclass=_ReifiedGenericMetaclass): 348 | """A ``Generic`` where the type parameters are available at runtime and is 349 | usable in ``isinstance`` and ``issubclass`` checks. 350 | 351 | For example: 352 | 353 | >>> class Foo(ReifiedGeneric[T]): 354 | ... def create_instance(self) -> T: 355 | ... cls = self.__reified_generics__[0] 356 | ... return cls() 357 | ... 358 | ... foo: Foo[int] = Foo() # error: generic cannot be reified 359 | ... foo = Foo[int]() # no error, as types have been supplied in a runtime position 360 | 361 | To define multiple generics, use a Tuple type: 362 | 363 | >>> class Foo(ReifiedGeneric[Tuple[T, U]]): 364 | ... pass 365 | ... 366 | ... foo = Foo[int, str]() 367 | 368 | Since the type parameters are guaranteed to be reified, that means ``isinstance`` 369 | and ``issubclass`` checks work as well: 370 | 371 | >>> isinstance(Foo[int, str](), Foo[int, int]) # type: ignore[misc] 372 | False 373 | 374 | note: basedmypy currently doesn't allow generics in ``isinstance`` and 375 | ``issubclass`` checks, so for now you have to use ``basedtyping.issubform`` for 376 | subclass checks and ``# type: ignore[misc]`` for instance checks. this issue 377 | is tracked [here](https://github.com/KotlinIsland/basedmypy/issues/5) 378 | """ 379 | 380 | __reified_generics__: tuple[type, ...] 381 | """Should be a generic but cant due to https://github.com/KotlinIsland/basedmypy/issues/142""" 382 | __type_vars__: tuple[TypeVar, ...] 383 | """``TypeVar``\\s that have not yet been reified. so this Tuple should always be\ 384 | empty by the time the ``ReifiedGeneric`` is instantiated""" 385 | 386 | @_tp_cache # type: ignore[no-any-expr, misc] 387 | def __class_getitem__( # type: ignore[no-any-decorated] 388 | cls, item: GenericItems 389 | ) -> type[ReifiedGeneric[T]]: 390 | # when defining the generic (ie. `class Foo(ReifiedGeneric[T]):`) we 391 | # want the normal behavior 392 | if cls is ReifiedGeneric: 393 | # https://github.com/KotlinIsland/basedtypeshed/issues/7 394 | return super().__class_getitem__(item) # type: ignore[misc, no-any-return] 395 | 396 | items = item if isinstance(item, tuple) else (item,) 397 | 398 | # if we're subtyping a class that already has reified generics: 399 | superclass_reified_generics = tuple( 400 | generic 401 | for generic in ( 402 | cls.__reified_generics__ if hasattr(cls, "__reified_generics__") else () 403 | ) 404 | # TODO: investigate this unreachable, redundant-expr # noqa: TD003 405 | if not isinstance(generic, TypeVar) # type: ignore[unused-ignore, unreachable, redundant-expr, no-any-expr] 406 | ) 407 | 408 | # normal generics use __parameters__, we use __type_vars__ because the 409 | # Generic base class deletes properties named __parameters__ when copying 410 | # to a new class 411 | orig_type_vars = ( 412 | cls.__type_vars__ 413 | if hasattr(cls, "__type_vars__") 414 | else cast( 415 | Tuple[TypeVar, ...], 416 | cls.__parameters__, # type:ignore[attr-defined] 417 | ) 418 | ) 419 | 420 | # add any reified generics from the superclass if there is one 421 | items = superclass_reified_generics + items 422 | expected_length = len(orig_type_vars) 423 | actual_length = len(items) - len(superclass_reified_generics) 424 | if expected_length != len(items) - len(superclass_reified_generics): 425 | raise NotEnoughTypeParametersError( 426 | "Incorrect number of type parameters specified. expected length:" 427 | f" {expected_length}, actual length {actual_length}" 428 | ) 429 | reified_generic_copy: type[ReifiedGeneric[T]] = type( 430 | cls.__name__, 431 | ( 432 | cls, # make the copied class extend the original so normal instance checks work 433 | ), 434 | # TODO: proper type # noqa: TD003 435 | { # type: ignore[no-any-expr] 436 | "__reified_generics__": tuple( # type: ignore[no-any-expr] 437 | _type_convert(t) 438 | for t in items # type: ignore[unused-ignore, no-any-expr] 439 | ), 440 | "_orig_type_vars": orig_type_vars, 441 | "__type_vars__": _collect_parameters(items), # type: ignore[name-defined] 442 | }, 443 | ) 444 | # can't set it in the dict above otherwise __init_subclass__ overwrites it 445 | reified_generic_copy._can_do_instance_and_subclass_checks_without_generics = False 446 | return reified_generic_copy 447 | 448 | @override 449 | def __init_subclass__(cls): 450 | cls._can_do_instance_and_subclass_checks_without_generics = True 451 | super().__init_subclass__() 452 | 453 | 454 | if sys.version_info >= (3, 10): 455 | from types import UnionType 456 | 457 | _UnionTypes = (UnionType, OldUnionType) 458 | _Forms: TypeAlias = type | UnionType | _SpecialForm | typing_extensions._SpecialForm # type: ignore[unused-ignore, no-any-expr] 459 | else: 460 | _UnionTypes = (OldUnionType,) 461 | _Forms: TypeAlias = Union[type, _SpecialForm, typing_extensions._SpecialForm] 462 | 463 | 464 | # TODO: make this work with any "form", not just unions # noqa: TD003 465 | # should be (form: TypeForm, forminfo: TypeForm) 466 | # TODO: form/forminfo can include _UnionGenericAlias # noqa: TD003 467 | def issubform(form: _Forms, forminfo: _Forms) -> bool: 468 | """EXPERIMENTAL: Warning, this function currently only supports unions and ``Never``. 469 | 470 | Returns ``True`` if ``form`` is a subform (specialform or subclass) of ``forminfo``. 471 | 472 | Like ``issubclass`` but supports typeforms (type-time types) 473 | 474 | for example: 475 | 476 | >>> issubclass(int | str, object) 477 | TypeError: issubclass() arg 1 must be a class 478 | 479 | >>> issubform(int | str, str) 480 | False 481 | 482 | >>> issubform(int | str, object) 483 | True 484 | """ 485 | if isinstance(form, _UnionTypes): 486 | # Morally, form is an instance of "UnionType | _UnionGenericAlias" 487 | # But _UnionGenericAlias doesn't have any representation at type time. 488 | return all( 489 | issubform(t, forminfo) 490 | for t in cast(Sequence[type], form.__args__) # type: ignore[union-attr] 491 | ) 492 | if sys.version_info < (3, 10) and isinstance(forminfo, OldUnionType): 493 | # Morally, forminfo is an instance of "_UnionGenericAlias" 494 | # But _UnionGenericAlias doesn't have any representation at type time. 495 | return any(issubform(form, t) for t in cast(Sequence[type], forminfo.__args__)) # type: ignore[union-attr] 496 | if form is Never: 497 | return True 498 | if forminfo is Never: 499 | return False 500 | return issubclass(form, forminfo) # type: ignore[arg-type] 501 | 502 | 503 | if BASEDMYPY_TYPE_CHECKING or not TYPE_CHECKING: 504 | 505 | @_BasedSpecialForm 506 | def Untyped( # noqa: N802 507 | self: _BasedSpecialForm, 508 | parameters: object, # noqa: ARG001 509 | ) -> NoReturn: 510 | """Special type indicating that something isn't typed. 511 | 512 | This is more specialized than ``Any`` and can help with gradually typing modules. 513 | """ 514 | raise TypeError(f"{self} is not subscriptable") 515 | else: 516 | # We pretend that it's an alias to Any so that it's slightly more compatible with 517 | # other tools 518 | Untyped: TypeAlias = Any # type: ignore[no-any-explicit] 519 | 520 | 521 | class _IntersectionGenericAlias(_BasedGenericAlias, _root=True): 522 | @override 523 | def copy_with(self, args: object) -> Self: # type: ignore[override] # TODO: put in the overloads # noqa: TD003 524 | return cast(Self, Intersection[args]) 525 | 526 | @override 527 | def __eq__(self, other: object) -> bool: 528 | if not isinstance(other, _IntersectionGenericAlias): 529 | return NotImplemented 530 | return set(self.__args__) == set(other.__args__) 531 | 532 | @override 533 | def __hash__(self) -> int: 534 | return hash(frozenset(self.__args__)) 535 | 536 | def __instancecheck__(self, obj: object) -> bool: 537 | return self.__subclasscheck__(type(obj)) 538 | 539 | def __subclasscheck__(self, cls: type[object]) -> bool: 540 | return all(issubclass(cls, arg) for arg in self.__args__) 541 | 542 | @override 543 | def __reduce__(self) -> (object, object): 544 | func, (_, args) = super().__reduce__() # type: ignore[no-any-expr, misc] 545 | return func, (Intersection, args) 546 | 547 | 548 | @_BasedSpecialForm 549 | def Intersection(self: _BasedSpecialForm, parameters: object) -> object: # noqa: N802 550 | """Intersection type; Intersection[X, Y] means both X and Y. 551 | 552 | To define an intersection: 553 | - If using __future__.annotations, shortform can be used e.g. A & B 554 | - otherwise the fullform must be used e.g. Intersection[A, B]. 555 | 556 | Details: 557 | - The arguments must be types and there must be at least one. 558 | - None as an argument is a special case and is replaced by 559 | type(None). 560 | - Intersections of intersections are flattened, e.g.:: 561 | 562 | Intersection[Intersection[int, str], float] == Intersection[int, str, float] 563 | 564 | - Intersections of a single argument vanish, e.g.:: 565 | 566 | Intersection[int] == int # The constructor actually returns int 567 | 568 | - Redundant arguments are skipped, e.g.:: 569 | 570 | Intersection[int, str, int] == Intersection[int, str] 571 | 572 | - When comparing intersections, the argument order is ignored, e.g.:: 573 | 574 | Intersection[int, str] == Intersection[str, int] 575 | 576 | - You cannot subclass or instantiate an intersection. 577 | """ 578 | if parameters == (): 579 | raise TypeError("Cannot take an Intersection of no types.") 580 | if not isinstance(parameters, tuple): 581 | parameters = (parameters,) 582 | msg = "Intersection[arg, ...]: each arg must be a type." 583 | parameters = tuple(_type_check(p, msg) for p in parameters) 584 | parameters = _remove_dups_flatten(parameters) # type: ignore[no-any-expr] 585 | if len(parameters) == 1: # type: ignore[no-any-expr] 586 | return parameters[0] # type: ignore[no-any-expr] 587 | return _IntersectionGenericAlias(self, parameters) # type: ignore[arg-type, no-any-expr] 588 | 589 | 590 | class _TypeFormForm(_BasedSpecialForm, _root=True): # type: ignore[misc] 591 | # TODO: decorator-ify # noqa: TD003 592 | def __init__(self, doc: str): 593 | self._name = "TypeForm" 594 | self._doc = self.__doc__ = doc 595 | 596 | @override 597 | def __getitem__(self, parameters: object | tuple[object]) -> _BasedGenericAlias: 598 | if not isinstance(parameters, tuple): 599 | parameters = (parameters,) 600 | 601 | return _BasedGenericAlias(self, parameters) # type: ignore[arg-type] 602 | 603 | 604 | TypeForm = _TypeFormForm( 605 | doc="""\ 606 | A type that can be used to represent a ``builtins.type`` or a ``SpecialForm``. 607 | For example: 608 | 609 | def f[T](t: TypeForm[T]) -> T: ... 610 | 611 | reveal_type(f(int | str)) # int | str 612 | """ 613 | ) 614 | 615 | 616 | def as_functiontype(fn: Callable[P, T]) -> FunctionType[P, T]: 617 | """Asserts that a ``Callable`` is a ``FunctionType`` and returns it 618 | 619 | best used as a decorator to fix other incorrectly typed decorators: 620 | 621 | def deco(fn: Callable[[], None]) -> Callable[[], None]: ... 622 | 623 | @as_functiontype 624 | @deco 625 | def foo(): ... 626 | """ 627 | if not isinstance(fn, types.FunctionType): 628 | raise TypeError(f"{fn} is not a FunctionType") 629 | return cast(FunctionType[P, T], fn) 630 | 631 | 632 | class ForwardRef(typing.ForwardRef, _root=True): # type: ignore[call-arg,misc] 633 | """ 634 | Like `typing.ForwardRef`, but lets older Python versions use newer typing features. 635 | Specifically, when evaluated, this transforms `X | Y` into `typing.Union[X, Y]` 636 | and `list[X]` into `typing.List[X]` etc. (for all the types made generic in PEP 585) 637 | if the original syntax is not supported in the current Python version. 638 | """ 639 | 640 | # older typing.ForwardRef doesn't have this 641 | if sys.version_info < (3, 10): 642 | __slots__ = ("__forward_module__", "__forward_is_class__") 643 | elif sys.version_info < (3, 11): 644 | __slots__ = ("__forward_is_class__",) 645 | 646 | def __init__(self, arg: str, *, is_argument=True, module: object = None, is_class=False): 647 | if not isinstance(arg, str): # type: ignore[redundant-expr] 648 | raise TypeError(f"Forward reference must be a string -- got {arg!r}") 649 | 650 | # If we do `def f(*args: *Ts)`, then we'll have `arg = '*Ts'`. 651 | # Unfortunately, this isn't a valid expression on its own, so we 652 | # do the unpacking manually. 653 | arg_to_compile = ( 654 | f"({arg},)[0]" # E.g. (*Ts,)[0] or (*tuple[int, int],)[0] 655 | if arg.startswith("*") 656 | else arg 657 | ) 658 | try: 659 | with warnings.catch_warnings(): 660 | # warnings come from some based syntax, i can't remember what 661 | warnings.simplefilter("ignore", category=SyntaxWarning) 662 | code = compile(arg_to_compile, "", "eval") 663 | except SyntaxError: 664 | try: 665 | ast.parse(arg_to_compile.removeprefix("def "), mode="func_type") 666 | except SyntaxError: 667 | raise SyntaxError(f"invalid syntax in ForwardRef: {arg_to_compile}?") from None 668 | else: 669 | code = compile("'un-representable callable type'", "", "eval") 670 | 671 | self.__forward_arg__ = arg 672 | self.__forward_code__ = code 673 | self.__forward_evaluated__ = False 674 | self.__forward_value__ = None 675 | self.__forward_is_argument__ = is_argument 676 | self.__forward_is_class__ = is_class 677 | self.__forward_module__ = module 678 | 679 | if sys.version_info >= (3, 13): 680 | 681 | @override 682 | def _evaluate( 683 | self, 684 | globalns: dict[str, object] | None, 685 | localns: Mapping[str, object] | None, 686 | type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = (), 687 | *, 688 | recursive_guard: frozenset[str], 689 | ) -> object | None: 690 | return transformer._eval_direct( 691 | self, globalns, localns if localns is None else dict(localns) 692 | ) 693 | 694 | elif sys.version_info >= (3, 12): 695 | 696 | @override 697 | def _evaluate( 698 | self, 699 | globalns: dict[str, object] | None, 700 | localns: Mapping[str, object] | None, 701 | type_params: tuple[TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...] | None = None, 702 | *, 703 | recursive_guard: frozenset[str], 704 | ) -> object | None: 705 | return transformer._eval_direct( 706 | self, globalns, localns if localns is None else dict(localns) 707 | ) 708 | 709 | else: 710 | 711 | @override 712 | def _evaluate( 713 | self, 714 | globalns: dict[str, object] | None, 715 | localns: Mapping[str, object] | None, 716 | recursive_guard: frozenset[str], 717 | ) -> object | None: 718 | return transformer._eval_direct( 719 | self, globalns, localns if localns is None else dict(localns) 720 | ) 721 | 722 | 723 | def _type_check(arg: object, msg: str) -> object: 724 | """Check that the argument is a type, and return it (internal helper). 725 | 726 | As a special case, accept None and return type(None) instead. Also wrap strings 727 | into ForwardRef instances. Consider several corner cases, for example plain 728 | special forms like Union are not valid, while Union[int, str] is OK, etc. 729 | The msg argument is a human-readable error message, e.g:: 730 | 731 | "Union[arg, ...]: arg should be a type." 732 | 733 | We append the repr() of the actual value (truncated to 100 chars). 734 | """ 735 | invalid_generic_forms = (Generic, typing.Protocol) 736 | 737 | arg = _type_convert(arg) 738 | if isinstance(arg, _GenericAlias) and arg.__origin__ in invalid_generic_forms: # type: ignore[comparison-overlap] 739 | raise TypeError(f"{arg} is not valid as type argument") 740 | if arg in (Any, NoReturn, typing.Final, Untyped): 741 | return arg 742 | if isinstance(arg, _SpecialForm) or arg in (Generic, typing.Protocol): 743 | raise TypeError(f"Plain {arg} is not valid as type argument") 744 | if isinstance(arg, (type, TypeVar, ForwardRef)): 745 | return arg 746 | if not callable(arg): 747 | raise TypeError(f"{msg} Got {arg!r:.100}.") 748 | return arg 749 | 750 | 751 | _strip_annotations = typing._strip_annotations # type: ignore[attr-defined] 752 | 753 | 754 | def get_type_hints( # type: ignore[no-any-explicit] 755 | obj: object 756 | | Callable[..., object] 757 | | FunctionType[..., object] 758 | | types.BuiltinFunctionType[..., object] 759 | | types.MethodType 760 | | types.ModuleType 761 | | types.WrapperDescriptorType 762 | | types.MethodWrapperType 763 | | types.MethodDescriptorType, 764 | globalns: dict[str, object] | None = None, 765 | localns: dict[str, object] | None = None, 766 | include_extras: bool = False, # noqa: FBT001, FBT002 767 | ) -> dict[str, object]: 768 | """Return type hints for an object. 769 | 770 | same as `typing.get_type_hints` except: 771 | - supports based typing denotations 772 | - adds the class to the scope: 773 | 774 | ```py 775 | class Base: 776 | def __init_subclass__(cls): 777 | get_type_hints(cls) 778 | 779 | class A(Base): 780 | a: A 781 | ``` 782 | """ 783 | if getattr(obj, "__no_type_check__", None): # type: ignore[no-any-expr] 784 | return {} 785 | # Classes require a special treatment. 786 | if isinstance(obj, type): # type: ignore[no-any-expr] 787 | hints = {} 788 | for base in reversed(obj.__mro__): 789 | if globalns is None: 790 | base_globals = getattr(sys.modules.get(base.__module__, None), "__dict__", {}) # type: ignore[no-any-expr] 791 | else: 792 | base_globals = globalns 793 | ann = base.__dict__.get("__annotations__", {}) # type: ignore[no-any-expr] 794 | if isinstance(ann, types.GetSetDescriptorType): # type: ignore[no-any-expr] 795 | ann = {} # type: ignore[no-any-expr] 796 | base_locals = dict(vars(base)) if localns is None else localns # type: ignore[no-any-expr] 797 | if localns is None and globalns is None: 798 | # This is surprising, but required. Before Python 3.10, 799 | # get_type_hints only evaluated the globalns of 800 | # a class. To maintain backwards compatibility, we reverse 801 | # the globalns and localns order so that eval() looks into 802 | # *base_globals* first rather than *base_locals*. 803 | # This only affects ForwardRefs. 804 | base_globals, base_locals = base_locals, base_globals 805 | # start not copied section 806 | if base is obj: 807 | # add the class to the scope 808 | base_locals[obj.__name__] = obj # type: ignore[no-any-expr] 809 | # end not copied section 810 | for name, value in ann.items(): # type: ignore[no-any-expr] 811 | if value is None: # type: ignore[no-any-expr] 812 | value = type(None) 813 | if isinstance(value, str): # type: ignore[no-any-expr] 814 | value = ForwardRef(value, is_argument=False, is_class=True) 815 | if sys.version_info < (3, 12): 816 | value = typing._eval_type(value, base_globals, base_locals) # type: ignore[attr-defined, no-any-expr] 817 | else: 818 | value = typing._eval_type( # type: ignore[attr-defined] 819 | value, # type: ignore[no-any-expr] 820 | base_globals, # type: ignore[no-any-expr] 821 | base_locals, # type: ignore[no-any-expr] 822 | base.__type_params__, 823 | ) 824 | hints[name] = value # type: ignore[no-any-expr] 825 | return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} # type: ignore[no-any-expr] 826 | 827 | if globalns is None: 828 | if isinstance(obj, types.ModuleType): # type: ignore[no-any-expr] 829 | globalns = obj.__dict__ 830 | else: 831 | nsobj = obj 832 | # Find globalns for the unwrapped object. 833 | while hasattr(nsobj, "__wrapped__"): 834 | nsobj = nsobj.__wrapped__ # type: ignore[no-any-expr] 835 | globalns = getattr(nsobj, "__globals__", {}) # type: ignore[no-any-expr] 836 | if localns is None: 837 | localns = globalns 838 | elif localns is None: 839 | localns = globalns 840 | hints = getattr(obj, "__annotations__", None) # type: ignore[assignment, no-any-expr] 841 | if hints is None: # type: ignore[no-any-expr, redundant-expr] 842 | # Return empty annotations for something that _could_ have them. 843 | if isinstance(obj, typing._allowed_types): # type: ignore[ unreachable] 844 | return {} 845 | raise TypeError(f"{obj!r} is not a module, class, method, or function.") 846 | hints = dict(hints) # type: ignore[no-any-expr] 847 | type_params = getattr(obj, "__type_params__", ()) # type: ignore[no-any-expr] 848 | for name, value in hints.items(): # type: ignore[no-any-expr] 849 | if value is None: # type: ignore[no-any-expr] 850 | value = type(None) 851 | if isinstance(value, str): # type: ignore[no-any-expr] 852 | # class-level forward refs were handled above, this must be either 853 | # a module-level annotation or a function argument annotation 854 | value = ForwardRef( 855 | value, 856 | is_argument=not isinstance(cast(object, obj), types.ModuleType), 857 | is_class=False, 858 | ) 859 | if sys.version_info >= (3, 12): 860 | hints[name] = typing._eval_type(value, globalns, localns, type_params=type_params) # type: ignore[no-any-expr, attr-defined] 861 | else: 862 | hints[name] = typing._eval_type(value, globalns, localns) # type: ignore[no-any-expr, attr-defined] 863 | return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} # type: ignore[no-any-expr] 864 | -------------------------------------------------------------------------------- /basedtyping/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/basedtyping/py.typed -------------------------------------------------------------------------------- /basedtyping/runtime_only.py: -------------------------------------------------------------------------------- 1 | """This module only works at runtime. the types defined here do not work as type annotations 2 | and are only intended for runtime checks, for example ``isinstance``. 3 | 4 | This is the similar to the ``types`` module. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from typing import Final, Final as Final_ext, Literal, Union 10 | 11 | LiteralType: Final = type(Literal[1]) 12 | """A type that can be used to check if type hints are a ``typing.Literal`` instance""" 13 | 14 | # TODO: this is type[object], we need it to be 'SpecialForm[Union]' (or something) 15 | # https://github.com/KotlinIsland/basedtyping/issues/53 16 | OldUnionType: Final_ext[type[object]] = type(Union[str, int]) 17 | """A type that can be used to check if type hints are a ``typing.Union`` instance.""" 18 | -------------------------------------------------------------------------------- /basedtyping/transformer.py: -------------------------------------------------------------------------------- 1 | """utilities to create standard compatible annotations""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | import sys 7 | import types 8 | import typing 9 | from contextlib import contextmanager 10 | from dataclasses import dataclass 11 | from enum import Enum 12 | from functools import partial 13 | from typing import cast 14 | 15 | import typing_extensions 16 | from typing_extensions import override 17 | 18 | import basedtyping 19 | 20 | 21 | @dataclass 22 | class EvalFailedError(TypeError): 23 | """Raised when `CringeTransformer.eval_type` fails""" 24 | 25 | message: str 26 | ref: typing.ForwardRef 27 | transformer: CringeTransformer 28 | 29 | 30 | # ruff: noqa: S101 erm, i wanted to use assert TODO: do something better 31 | class CringeTransformer(ast.NodeTransformer): 32 | """Transforms `1 | 2` into `Literal[1] | Literal[2]` etc""" 33 | 34 | def __init__( 35 | self, 36 | globalns: dict[str, object] | None, 37 | localns: dict[str, object] | None, 38 | *, 39 | string_literals: bool, 40 | ): 41 | self.string_literals = string_literals 42 | 43 | # This logic for handling Nones is copied from typing.ForwardRef._evaluate 44 | if globalns is None and localns is None: 45 | globalns = localns = {} 46 | elif globalns is None: 47 | assert localns is not None 48 | globalns = localns 49 | elif localns is None: 50 | assert globalns is not None 51 | localns = globalns 52 | 53 | fair_and_unique_uuid_roll = "c4357574960843a2a8f9eb0c11aa88e5" 54 | self.typing_name = f"_typing_extensions_{fair_and_unique_uuid_roll}" 55 | self.basedtyping_name = f"_basedtyping_{fair_and_unique_uuid_roll}" 56 | self.globalns = globalns 57 | 58 | self.localns = localns | { 59 | self.typing_name: typing_extensions, 60 | self.basedtyping_name: basedtyping, 61 | } 62 | 63 | @override 64 | def visit(self, node: ast.AST) -> ast.AST: 65 | return cast(ast.AST, super().visit(node)) 66 | 67 | def eval_type( 68 | self, 69 | node: ast.FunctionType | ast.Expression | ast.expr, 70 | *, 71 | original_ref: typing.ForwardRef | None = None, 72 | ) -> object: 73 | if isinstance(node, ast.expr): 74 | node = ast.copy_location(ast.Expression(node), node) 75 | ref = typing.ForwardRef(ast.unparse(node)) 76 | if original_ref: 77 | for attr in ("is_argument", " is_class", "module"): 78 | attr = f"__forward_{attr}__" 79 | if hasattr(original_ref, attr): 80 | setattr(ref, attr, cast(object, getattr(original_ref, attr))) 81 | if not isinstance(node, ast.FunctionType): 82 | ref.__forward_code__ = compile(node, "", "eval") 83 | try: 84 | type_ = typing._type_convert( # type: ignore[attr-defined] 85 | cast(object, eval(ref.__forward_code__, self.globalns, self.localns)) # noqa: S307 86 | ) 87 | if sys.version_info >= (3, 13): 88 | return typing._eval_type( # type: ignore[attr-defined] 89 | type_, self.globalns, self.localns, type_params=() 90 | ) 91 | else: # noqa: RET505 mypy prefers it in different branches TODO: raise an issue 92 | return typing._eval_type( # type: ignore[attr-defined] 93 | type_, self.globalns, self.localns 94 | ) 95 | except TypeError as e: 96 | raise EvalFailedError(str(e), ref, self) from e 97 | 98 | def _typing(self, attr: str) -> ast.Attribute: 99 | result = ast.Attribute( 100 | value=ast.Name(id=self.typing_name, ctx=ast.Load()), attr=attr, ctx=ast.Load() 101 | ) 102 | return ast.fix_missing_locations(result) 103 | 104 | def _basedtyping(self, attr: str) -> ast.Attribute: 105 | result = ast.Attribute( 106 | value=ast.Name(id=self.basedtyping_name, ctx=ast.Load()), attr=attr, ctx=ast.Load() 107 | ) 108 | return ast.fix_missing_locations(result) 109 | 110 | def _literal( 111 | self, value: ast.Constant | ast.Name | ast.Attribute | ast.UnaryOp 112 | ) -> ast.Subscript: 113 | return self.subscript(self._typing("Literal"), value) 114 | 115 | def subscript(self, value: ast.expr, slice_: ast.expr) -> ast.Subscript: 116 | result = ast.Subscript(value=value, slice=slice_, ctx=ast.Load()) 117 | return ast.fix_missing_locations(result) 118 | 119 | _implicit_tuple = False 120 | 121 | @contextmanager 122 | def implicit_tuple(self, *, value=True) -> typing.Iterator[None]: 123 | implicit_tuple = self._implicit_tuple 124 | self._implicit_tuple = value 125 | try: 126 | yield 127 | finally: 128 | self._implicit_tuple = implicit_tuple 129 | 130 | @override 131 | def visit_Subscript(self, node: ast.Subscript) -> ast.AST: 132 | node_type = self.eval_type(node.value) 133 | if self.eval_type(node.value) is typing_extensions.Literal: 134 | return node 135 | if node_type is typing_extensions.Annotated: 136 | slice_ = node.slice 137 | if isinstance(slice_, ast.Tuple): 138 | temp = self.visit(slice_.elts[0]) 139 | assert isinstance(temp, ast.expr) 140 | slice_.elts[0] = temp 141 | else: 142 | temp = self.visit(slice_) 143 | assert isinstance(temp, ast.expr) 144 | node.slice = temp 145 | return node 146 | with self.implicit_tuple(): 147 | result = self.generic_visit(node) 148 | assert isinstance(result, ast.Subscript) 149 | node = result 150 | 151 | node_type = self.eval_type(node.value) 152 | if node_type is types.FunctionType: 153 | slice2_ = node.slice 154 | node = self.subscript(self._typing("Callable"), slice2_) 155 | return node 156 | 157 | @override 158 | def visit_Attribute(self, node: ast.Attribute) -> ast.AST: 159 | node = self.generic_visit(node) 160 | assert isinstance(node, ast.expr) 161 | node_type = self.eval_type(node) 162 | if isinstance(node_type, Enum): 163 | assert isinstance(node, (ast.Name, ast.Attribute)) 164 | return self._literal(node) 165 | return node 166 | 167 | @override 168 | def visit_Name(self, node: ast.Name) -> ast.AST: 169 | name_type = self.eval_type(node) 170 | if isinstance(name_type, Enum): 171 | return self._literal(node) 172 | return node 173 | 174 | @override 175 | def visit_Constant(self, node: ast.Constant) -> ast.AST: 176 | value = cast(object, node.value) 177 | if not self.string_literals and isinstance(value, str): 178 | return self._transform(basedtyping.ForwardRef(value)).body 179 | if isinstance(value, int) or (self.string_literals and isinstance(value, str)): 180 | return self._literal(node) 181 | return node 182 | 183 | @override 184 | def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST: 185 | if not isinstance(node.operand, ast.Constant): 186 | return node 187 | if not isinstance(node.op, (ast.UAdd, ast.USub)): 188 | return node 189 | return self._literal(node) 190 | 191 | @override 192 | def visit_Tuple(self, node: ast.Tuple) -> ast.AST: 193 | with self.implicit_tuple(value=False): 194 | result = self.generic_visit(node) 195 | if not self._implicit_tuple: 196 | return self.subscript(self._typing("Tuple"), cast(ast.expr, result)) 197 | return result 198 | 199 | @override 200 | def visit_Compare(self, node: ast.Compare) -> ast.AST: 201 | if len(node.ops) == 1 and isinstance(node.ops[0], ast.Is): 202 | result = self.subscript( 203 | self._typing("TypeIs"), cast(ast.expr, self.generic_visit(node.comparators[0])) 204 | ) 205 | return self.generic_visit(result) 206 | return self.generic_visit(node) 207 | 208 | @override 209 | def visit_IfExp(self, node: ast.IfExp) -> ast.AST: 210 | if ( 211 | isinstance(node.body, ast.Compare) 212 | and len(node.body.comparators) == 1 213 | and isinstance(node.body.ops[0], ast.Is) 214 | ): 215 | node.body = self.subscript( 216 | self._typing("TypeGuard"), 217 | cast(ast.expr, self.generic_visit(node.body.comparators[0])), 218 | ) 219 | return self.generic_visit(node) 220 | 221 | def visit_FunctionType(self, node: ast.FunctionType) -> ast.AST: # noqa: N802 https://github.com/KotlinIsland/basedmypy/issues/763 222 | node = self.generic_visit(node) 223 | assert isinstance(node, ast.FunctionType) 224 | return ast.Expression( 225 | self.subscript( 226 | self._typing("Callable"), 227 | ast.Tuple([ast.List(node.argtypes, ctx=ast.Load()), node.returns], ctx=ast.Load()), 228 | ) 229 | ) 230 | 231 | @override 232 | def visit_BinOp(self, node: ast.BinOp) -> ast.AST: 233 | node = self.generic_visit(node) 234 | if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitAnd): 235 | node = self.subscript( 236 | self._basedtyping("Intersection"), 237 | ast.Tuple([node.left, node.right], ctx=ast.Load()), 238 | ) 239 | return node 240 | 241 | def _transform(self, value: typing.ForwardRef) -> ast.Expression: 242 | tree: ast.AST 243 | try: 244 | tree = ast.parse(value.__forward_arg__, mode="eval") 245 | except SyntaxError: 246 | arg = value.__forward_arg__.lstrip() 247 | if arg.startswith(("def ", "def(")): 248 | arg = arg[3:].lstrip() 249 | tree = ast.parse(arg, mode="func_type") 250 | 251 | tree = self.visit(tree) 252 | assert isinstance(tree, ast.Expression) 253 | return tree 254 | 255 | 256 | def eval_type_based( 257 | value: object, 258 | globalns: dict[str, object] | None = None, 259 | localns: dict[str, object] | None = None, 260 | *, 261 | string_literals: bool, 262 | ) -> object: 263 | """Like `typing._eval_type`, but supports based typing features. 264 | Specifically, this transforms `1 | 2` into `typing.Union[Literal[1], Literal[2]]` 265 | and `(int) -> str` into `typing.Callable[[int], str]` etc. 266 | """ 267 | if not isinstance(value, typing.ForwardRef): 268 | return value 269 | transformer = CringeTransformer(globalns, localns, string_literals=string_literals) 270 | tree = transformer._transform(value) 271 | return transformer.eval_type(tree, original_ref=value) 272 | 273 | 274 | if typing.TYPE_CHECKING: 275 | 276 | def _eval_direct( 277 | value: object, # noqa: ARG001 278 | globalns: dict[str, object] | None = None, # noqa: ARG001 279 | localns: dict[str, object] | None = None, # noqa: ARG001 280 | ) -> object: 281 | ... 282 | else: 283 | _eval_direct = partial(eval_type_based, string_literals=False) 284 | -------------------------------------------------------------------------------- /basedtyping/typetime_only.py: -------------------------------------------------------------------------------- 1 | """This module only works at type-time, and cannot be used at runtime. 2 | 3 | This is the similar to the ``_typeshed`` module. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import TYPE_CHECKING, Generic 9 | 10 | from basedtyping import T 11 | 12 | if not TYPE_CHECKING: 13 | raise ImportError( 14 | "The ``basedtyping.typetime_only`` module cannot be imported at runtime. " 15 | "You should only import it within an ``if TYPE_CHECKING:`` block." 16 | ) 17 | 18 | 19 | class assert_type(Generic[T]): # noqa: N801 20 | """Used to assert that a value is type ``T``. 21 | 22 | note: This is more like a function than a class, 23 | but it's defined as a class so that you can explicitly specify the generic. 24 | """ 25 | 26 | # TODO: deprecate this # noqa: TD003 27 | # TODO: make this use ReifiedGeneric so that it can check at runtime 28 | # https://github.com/KotlinIsland/basedtyping/issues/15 29 | # None return type on __new__ is supported in pyright but not mypy 30 | def __new__(cls, _value: T): # type: ignore[empty-body] 31 | pass 32 | -------------------------------------------------------------------------------- /intellij/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .intellijPlatform 4 | .qodana 5 | build 6 | -------------------------------------------------------------------------------- /intellij/.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /intellij/.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | true 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /intellij/.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /intellij/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Basedtyping IntelliJ Plugin Changelog 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.1.4] 8 | 9 | ### Fixed 10 | 11 | - `Final` types infer correctly 12 | 13 | ## [0.1.3] 14 | 15 | ### Added 16 | 17 | - Callable literal types `() -> int` 18 | 19 | ### Fixed 20 | 21 | - Narrowed types were broken 22 | 23 | ## [0.1.2] - 2023-01-12 24 | 25 | ### Fixed 26 | 27 | - All functions returning `Any` 28 | 29 | ## [0.1.1] - 2024-1-5 30 | 31 | ### Fixed 32 | 33 | - Constructors incorrectly returning `None` 34 | 35 | ## [0.1.0] - 2023-12-20 36 | 37 | ### Added 38 | 39 | - Bare literal 40 | - Tuple literal types 41 | - Infer from overloads 42 | -------------------------------------------------------------------------------- /intellij/README.md: -------------------------------------------------------------------------------- 1 | # Basedtyping IntelliJ Plugin 2 | 3 | 4 | The Basedtyping plugin for IntelliJ IDEA aims to support based typing features and the 'basedtyping' package directly within the IDE environment. 5 | 6 | With this plugin, developers can validate and improve the quality of the code as it adheres to the 'basedtyping' standards. In adding type annotations and ensuring that they are correct, the developers can minimize runtime errors significantly. 7 | 8 | Features: 9 | - Bare literal 10 | - Tuple literal types 11 | - Infer from overloads 12 | 13 | The Basedtyping IntelliJ Plugin is a significant tool for Python developers who emphasize type safety and high-quality code. 14 | 15 | Get the Basedtyping IntelliJ Plugin now and maintain Python code with efficiency and ease. 16 | 17 | -------------------------------------------------------------------------------- /intellij/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | import org.jetbrains.intellij.platform.gradle.TestFrameworkType 4 | 5 | plugins { 6 | alias(libs.plugins.kotlin) // Kotlin support 7 | alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin 8 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 9 | alias(libs.plugins.kover) // Gradle Kover Plugin 10 | } 11 | 12 | group = providers.gradleProperty("pluginGroup").get() 13 | version = providers.gradleProperty("pluginVersion").get() 14 | 15 | // Set the JVM language level used to build the project. 16 | kotlin { 17 | jvmToolchain(17) 18 | } 19 | 20 | // Configure project's dependencies 21 | repositories { 22 | mavenCentral() 23 | 24 | // IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html 25 | intellijPlatform { 26 | defaultRepositories() 27 | } 28 | } 29 | 30 | // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog 31 | dependencies { 32 | testImplementation(libs.junit) 33 | 34 | // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html 35 | intellijPlatform { 36 | create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) 37 | 38 | // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. 39 | bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) 40 | 41 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. 42 | plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) 43 | 44 | instrumentationTools() 45 | pluginVerifier() 46 | zipSigner() 47 | testFramework(TestFrameworkType.Platform) 48 | } 49 | } 50 | 51 | // Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html 52 | intellijPlatform { 53 | 54 | // remove this if config options are ever added 55 | buildSearchableOptions = false 56 | 57 | pluginConfiguration { 58 | version = providers.gradleProperty("pluginVersion") 59 | 60 | // Extract the section from README.md and provide for the plugin's manifest 61 | description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 62 | val start = "" 63 | val end = "" 64 | 65 | with(it.lines()) { 66 | if (!containsAll(listOf(start, end))) { 67 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 68 | } 69 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 70 | } 71 | } 72 | 73 | val changelog = project.changelog // local variable for configuration cache compatibility 74 | // Get the latest available change notes from the changelog file 75 | changeNotes = providers.gradleProperty("pluginVersion").map { pluginVersion -> 76 | with(changelog) { 77 | renderItem( 78 | (getOrNull(pluginVersion) ?: getUnreleased()) 79 | .withHeader(false) 80 | .withEmptySections(false), 81 | Changelog.OutputType.HTML, 82 | ) 83 | } 84 | } 85 | 86 | ideaVersion { 87 | sinceBuild = providers.gradleProperty("pluginSinceBuild") 88 | untilBuild = providers.gradleProperty("pluginUntilBuild") 89 | } 90 | } 91 | 92 | signing { 93 | certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") 94 | privateKey = providers.environmentVariable("PRIVATE_KEY") 95 | password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") 96 | } 97 | 98 | publishing { 99 | token = providers.environmentVariable("PUBLISH_TOKEN") 100 | // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 101 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 102 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 103 | channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } 104 | } 105 | 106 | pluginVerification { 107 | ides { 108 | recommended() 109 | } 110 | } 111 | } 112 | 113 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 114 | changelog { 115 | groups.empty() 116 | repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") 117 | } 118 | 119 | // Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration 120 | kover { 121 | reports { 122 | total { 123 | xml { 124 | onCheck = true 125 | } 126 | } 127 | } 128 | } 129 | 130 | tasks { 131 | wrapper { 132 | gradleVersion = providers.gradleProperty("gradleVersion").get() 133 | } 134 | 135 | publishPlugin { 136 | dependsOn(patchChangelog) 137 | } 138 | } 139 | 140 | intellijPlatformTesting { 141 | runIde { 142 | register("runIdeForUiTests") { 143 | task { 144 | jvmArgumentProviders += CommandLineArgumentProvider { 145 | listOf( 146 | "-Drobot-server.port=8082", 147 | "-Dide.mac.message.dialogs.as.sheets=false", 148 | "-Djb.privacy.policy.text=", 149 | "-Djb.consents.confirmation.enabled=false", 150 | ) 151 | } 152 | } 153 | 154 | plugins { 155 | robotServerPlugin() 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /intellij/gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = org.basedsoft.plugins.basedtyping 4 | pluginName = basedtyping 5 | pluginRepositoryUrl = https://github.com/KotlinIsland/basedtyping 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 0.1.4+243 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 233 11 | pluginUntilBuild = 243.* 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = PC 15 | platformVersion = 2023.3.7 16 | 17 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 18 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP 19 | platformPlugins = 20 | # Example: platformBundledPlugins = com.intellij.java 21 | platformBundledPlugins = PythonCore 22 | 23 | # Gradle Releases -> https://github.com/gradle/gradle/releases 24 | gradleVersion = 8.10.2 25 | 26 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 27 | kotlin.stdlib.default.dependency = false 28 | 29 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 30 | org.gradle.configuration-cache = true 31 | 32 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 33 | org.gradle.caching = true 34 | -------------------------------------------------------------------------------- /intellij/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | 5 | # plugins 6 | changelog = "2.2.1" 7 | intelliJPlatform = "2.1.0" 8 | kotlin = "1.9.25" 9 | kover = "0.8.3" 10 | 11 | [libraries] 12 | junit = { group = "junit", name = "junit", version.ref = "junit" } 13 | 14 | [plugins] 15 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 16 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 17 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 18 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 19 | -------------------------------------------------------------------------------- /intellij/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/intellij/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /intellij/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /intellij/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /intellij/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /intellij/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 3 | } 4 | 5 | rootProject.name = "basedtyping" 6 | -------------------------------------------------------------------------------- /intellij/src/main/kotlin/org/basedsoft/plugins/basedtyping/BasedTypingTypeProvider.kt: -------------------------------------------------------------------------------- 1 | package org.basedsoft.plugins.basedtyping 2 | 3 | import com.intellij.openapi.util.Key 4 | import com.intellij.openapi.util.Ref 5 | import com.intellij.psi.PsiElement 6 | import com.intellij.psi.impl.source.resolve.FileContextUtil 7 | import com.intellij.psi.util.siblings 8 | import com.jetbrains.python.PyTokenTypes 9 | import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider 10 | import com.jetbrains.python.psi.* 11 | import com.jetbrains.python.psi.resolve.PyResolveContext 12 | import com.jetbrains.python.psi.types.* 13 | import com.jetbrains.python.psi.types.PyLiteralType.Companion.fromLiteralParameter 14 | 15 | private class BasedTypingTypeProvider : PyTypeProviderBase() { 16 | override fun getReferenceType(referenceTarget: PsiElement, context: TypeEvalContext, anchor: PsiElement?): Ref? { 17 | if (referenceTarget !is PyTargetExpression) return null 18 | val annotation = referenceTarget.annotation?.value ?: return null 19 | return Ref.create(getType(annotation, context) ?: return null) 20 | } 21 | 22 | override fun getParameterType(param: PyNamedParameter, func: PyFunction, context: TypeEvalContext): Ref? { 23 | return param.annotation?.value?.let { 24 | getType(it, context).ref() 25 | } ?: getOverload(param, func, context).ref() 26 | } 27 | 28 | override fun getReturnType(callable: PyCallable, context: TypeEvalContext): Ref? { 29 | if (callable !is PyFunction) return null 30 | 31 | return callable.annotation?.value?.let { annotation -> 32 | getType(annotation, context).ref() 33 | } ?: getOverloadReturn(callable, context).ref() 34 | } 35 | 36 | override fun getCallType(function: PyFunction, callSite: PyCallSiteExpression, context: TypeEvalContext): Ref? { 37 | val annotation = function.annotation?.value ?: return null 38 | return getType(annotation, context, simple = true).ref() 39 | } 40 | 41 | /** 42 | * Needed to work around a limitation in PyTypingTypeProvider 43 | */ 44 | override fun getReferenceExpressionType(referenceExpression: PyReferenceExpression, context: TypeEvalContext): PyType? { 45 | val param = referenceExpression.followAssignmentsChain(PyResolveContext.defaultContext(context)).element 46 | if (param !is PyNamedParameter) return null 47 | val annotation = param.annotation?.value ?: return null 48 | return getType(annotation, context, simple=true) 49 | } 50 | /** 51 | * Needed to work around a limitation in PyTypingTypeProvider 52 | */ 53 | override fun getCallableType(callable: PyCallable, context: TypeEvalContext): PyType? { 54 | if (callable !is PyFunction) return null 55 | return BasedPyFunctionTypeImpl(callable) 56 | } 57 | } 58 | 59 | /** 60 | * Needed to work around a limitation in PyTypingTypeProvider 61 | */ 62 | class BasedPyFunctionTypeImpl(val callable: PyFunction) : PyFunctionTypeImpl(callable) { 63 | override fun getReturnType(context: TypeEvalContext): PyType? { 64 | return callable.annotation?.value?.let { getType(it, context) } ?: super.getReturnType(context) 65 | } 66 | } 67 | 68 | fun getType(expression: PyExpression, context: TypeEvalContext, simple: Boolean = false): PyType? { 69 | return getLiteralType(expression, context) 70 | ?: getUnionType(expression, context) 71 | ?: getTupleType(expression, context) 72 | ?: getStringType(expression, context) 73 | ?: if (simple) null else PyTypingTypeProvider.getType(expression, context)?.get() 74 | } 75 | 76 | fun getLiteralType(target: PyExpression, context: TypeEvalContext): PyType? { 77 | if (target is PyNumericLiteralExpression || target is PyBoolLiteralExpression) { 78 | return fromLiteralParameter(target, context) 79 | } 80 | return null 81 | } 82 | 83 | fun getUnionType(target: PyExpression, context: TypeEvalContext): PyType? { 84 | if (target !is PyBinaryExpression) return null 85 | return when (target.operator) { 86 | PyTokenTypes.OR -> target.rightExpression?.let { right -> 87 | PyUnionType.union( 88 | getType(target.leftExpression, context), 89 | getType(right, context), 90 | ) 91 | } 92 | // HACK 93 | PyTokenTypes.AND -> getType(target.leftExpression, context) 94 | else -> null 95 | } 96 | } 97 | 98 | fun getTupleType(target: PyExpression, context: TypeEvalContext): PyType? { 99 | return when (target) { 100 | is PyParenthesizedExpression -> target.containedExpression ?: return null 101 | is PyTupleExpression -> target 102 | else -> return null 103 | }.children.map { getType(it as PyExpression, context) }.let { PyTupleType.create(target, it) } 104 | } 105 | 106 | fun getStringType(target: PyExpression, context: TypeEvalContext): PyType? { 107 | if (target !is PyStringLiteralExpression) return null 108 | val value = target.stringValue.trim() 109 | if (!((value.startsWith("def (") || value.startsWith("(")) && "->" in value)) return null 110 | val (l, r) = value.split("->") 111 | val argsExpressionPart1 = toExpression(l.removePrefix("def").trim(), target) 112 | val argsExpression = when (argsExpressionPart1) { 113 | is PyTupleExpression -> argsExpressionPart1 114 | is PyParenthesizedExpression -> argsExpressionPart1.containedExpression!! 115 | else -> return null 116 | } 117 | val args = when (argsExpression) { 118 | is PyTupleExpression -> argsExpression.map { getType(it, context) } 119 | else -> listOf(getType(argsExpression, context)) 120 | } 121 | val returnType = getType(toExpression(r.trim(), target) ?: return null, context) 122 | return PyCallableTypeImpl(args.map { PyCallableParameterImpl.nonPsi(it) }, returnType) 123 | } 124 | 125 | fun PyFunction.collectOverloads() = 126 | siblings(forward = false) 127 | .filterIsInstance(PyFunction::class.java) 128 | .filter { it.name == name } 129 | 130 | fun getOverload(param: PyNamedParameter, func: PyFunction, context: TypeEvalContext) = 131 | func.collectOverloads() 132 | .map { funcItem -> 133 | funcItem.parameterList.findParameterByName(param.name!!)?.annotation?.value?.let { 134 | getType(it, context) 135 | } 136 | } 137 | .filterNotNull() 138 | .toSet() 139 | .takeIf { it.isNotEmpty() } 140 | // TODO: keep the correct order 141 | ?.let { PyUnionType.union(it) } 142 | 143 | fun getOverloadReturn(func: PyFunction, context: TypeEvalContext) = 144 | func.collectOverloads() 145 | .map { funcItem -> 146 | funcItem.annotation?.value?.let { 147 | getType(it, context) 148 | } 149 | } 150 | .filterNotNull() 151 | .toSet() 152 | .takeIf { it.isNotEmpty() } 153 | ?.let { PyUnionType.union(it) } 154 | 155 | val FRAGMENT_OWNER: Key = Key.create("PY_FRAGMENT_OWNER") 156 | 157 | fun toExpression(contents: String, anchor: PsiElement): PyExpression? { 158 | val file = FileContextUtil.getContextFile(anchor) 159 | if (file == null) return null 160 | val fragment = PyUtil.createExpressionFromFragment(contents, file) 161 | if (fragment != null) { 162 | fragment.getContainingFile().putUserData(FRAGMENT_OWNER, anchor) 163 | } 164 | return fragment 165 | } 166 | 167 | fun PyType?.ref() = this?.let { Ref.create(it) } -------------------------------------------------------------------------------- /intellij/src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.basedsoft.plugins.basedtyping 4 | Basedtyping 5 | BasedSoft 6 | 7 | com.intellij.modules.platform 8 | com.intellij.modules.python 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /intellij/src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /intellij/src/test/java/com/jetbrains/python/PythonMockSdk.java: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | package com.jetbrains.python; 3 | 4 | import com.intellij.openapi.application.Application; 5 | import com.intellij.openapi.application.ApplicationManager; 6 | import com.intellij.openapi.projectRoots.*; 7 | import com.intellij.openapi.roots.OrderRootType; 8 | import com.intellij.openapi.util.KeyWithDefaultValue; 9 | import com.intellij.openapi.vfs.LocalFileSystem; 10 | import com.intellij.openapi.vfs.VirtualFile; 11 | import com.intellij.util.containers.ContainerUtil; 12 | import com.jetbrains.python.codeInsight.typing.PyTypeShed; 13 | import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil; 14 | import com.jetbrains.python.psi.LanguageLevel; 15 | import com.jetbrains.python.sdk.PythonSdkUtil; 16 | import org.jdom.Element; 17 | import org.jetbrains.annotations.NonNls; 18 | import org.jetbrains.annotations.NotNull; 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import java.io.File; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | public final class PythonMockSdk { 27 | 28 | private PythonMockSdk() { 29 | } 30 | 31 | public static @NotNull Sdk create() { 32 | return create(LanguageLevel.getLatest()); 33 | } 34 | 35 | public static @NotNull Sdk create(@NotNull String sdkPath) { 36 | return create(sdkPath, LanguageLevel.getLatest()); 37 | } 38 | 39 | public static @NotNull Sdk create(@NotNull LanguageLevel level, VirtualFile @NotNull ... additionalRoots) { 40 | return create(PythonHelpersLocator.getPythonCommunityPath() + "/testData" + "/MockSdk", level, additionalRoots); 41 | } 42 | 43 | private static @NotNull Sdk create(@NotNull String sdkPath, @NotNull LanguageLevel level, VirtualFile @NotNull ... additionalRoots) { 44 | String sdkName = "Mock " + PyNames.PYTHON_SDK_ID_NAME + " " + level.toPythonVersion(); 45 | return create(sdkName, sdkPath, new PyMockSdkType(level), level, additionalRoots); 46 | } 47 | 48 | public static @NotNull Sdk create(@NotNull String sdkName, 49 | @NotNull String sdkPath, 50 | @NotNull SdkTypeId sdkType, 51 | @NotNull LanguageLevel level, 52 | VirtualFile @NotNull ... additionalRoots) { 53 | Sdk sdk = ProjectJdkTable.getInstance().createSdk(sdkName, sdkType); 54 | SdkModificator sdkModificator = sdk.getSdkModificator(); 55 | sdkModificator.setHomePath(sdkPath + "/bin/python"); 56 | sdkModificator.setVersionString(toVersionString(level)); 57 | 58 | createRoots(sdkPath, level).forEach(vFile -> { 59 | sdkModificator.addRoot(vFile, OrderRootType.CLASSES); 60 | }); 61 | 62 | Arrays.asList(additionalRoots).forEach(vFile -> { 63 | sdkModificator.addRoot(vFile, OrderRootType.CLASSES); 64 | }); 65 | 66 | Application application = ApplicationManager.getApplication(); 67 | Runnable runnable = () -> sdkModificator.commitChanges(); 68 | if (application.isDispatchThread()) { 69 | application.runWriteAction(runnable); 70 | } else { 71 | application.invokeAndWait(() -> application.runWriteAction(runnable)); 72 | } 73 | sdk.putUserData(KeyWithDefaultValue.create("MOCK_PY_MARKER_KEY", true), true); 74 | return sdk; 75 | 76 | // com.jetbrains.python.psi.resolve.PythonSdkPathCache.getInstance() corrupts SDK, so have to clone 77 | //return sdk.clone(); 78 | } 79 | 80 | private static @NotNull List createRoots(@NotNull @NonNls String mockSdkPath, @NotNull LanguageLevel level) { 81 | final var result = new ArrayList(); 82 | 83 | final var localFS = LocalFileSystem.getInstance(); 84 | ContainerUtil.addIfNotNull(result, localFS.refreshAndFindFileByIoFile(new File(mockSdkPath, "Lib"))); 85 | ContainerUtil.addIfNotNull(result, localFS.refreshAndFindFileByIoFile(new File(mockSdkPath, PythonSdkUtil.SKELETON_DIR_NAME))); 86 | 87 | ContainerUtil.addIfNotNull(result, PyUserSkeletonsUtil.getUserSkeletonsDirectory()); 88 | 89 | result.addAll(PyTypeShed.INSTANCE.findRootsForLanguageLevel(level)); 90 | 91 | return result; 92 | } 93 | 94 | private static @NotNull String toVersionString(@NotNull LanguageLevel level) { 95 | return "Python " + level.toPythonVersion(); 96 | } 97 | 98 | private static final class PyMockSdkType implements SdkTypeId { 99 | 100 | @NotNull 101 | private final LanguageLevel myLevel; 102 | 103 | private PyMockSdkType(@NotNull LanguageLevel level) { 104 | myLevel = level; 105 | } 106 | 107 | @NotNull 108 | @Override 109 | public String getName() { 110 | return PyNames.PYTHON_SDK_ID_NAME; 111 | } 112 | 113 | @Override 114 | public @NotNull String getVersionString(@NotNull Sdk sdk) { 115 | return toVersionString(myLevel); 116 | } 117 | 118 | @Override 119 | public void saveAdditionalData(@NotNull SdkAdditionalData additionalData, @NotNull Element additional) { 120 | } 121 | 122 | @Nullable 123 | @Override 124 | public SdkAdditionalData loadAdditionalData(@NotNull Sdk currentSdk, @NotNull Element additional) { 125 | return null; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /intellij/src/test/java/com/jetbrains/python/README.md: -------------------------------------------------------------------------------- 1 | This stuff is copied from JetBrains/intellij-community because they don't ship it -------------------------------------------------------------------------------- /intellij/src/test/java/com/jetbrains/python/fixtures/PyLightProjectDescriptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2000-2015 JetBrains s.r.o. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.jetbrains.python.fixtures; 17 | 18 | import com.intellij.openapi.application.PathManager; 19 | import com.intellij.openapi.projectRoots.Sdk; 20 | import com.intellij.openapi.roots.ModifiableRootModel; 21 | import com.intellij.openapi.roots.OrderRootType; 22 | import com.intellij.openapi.roots.libraries.Library; 23 | import com.intellij.openapi.vfs.LocalFileSystem; 24 | import com.intellij.openapi.vfs.VirtualFile; 25 | import com.intellij.testFramework.LightProjectDescriptor; 26 | import com.jetbrains.python.PythonHelpersLocator; 27 | import com.jetbrains.python.PythonMockSdk; 28 | import com.jetbrains.python.psi.LanguageLevel; 29 | import org.jetbrains.annotations.NotNull; 30 | import org.jetbrains.annotations.Nullable; 31 | 32 | /** 33 | * Project descriptor (extracted from {@link com.jetbrains.python.fixtures.PyTestCase}) and should be used with it. 34 | * @author Ilya.Kazakevich 35 | */ 36 | public class PyLightProjectDescriptor extends LightProjectDescriptor { 37 | 38 | @Nullable 39 | private final String myName; 40 | 41 | @NotNull 42 | private final LanguageLevel myLevel; 43 | 44 | public PyLightProjectDescriptor(@NotNull LanguageLevel level) { 45 | this(null, level); 46 | } 47 | 48 | public PyLightProjectDescriptor(@NotNull String name) { 49 | this(name, LanguageLevel.getLatest()); 50 | } 51 | 52 | private PyLightProjectDescriptor(@Nullable String name, @NotNull LanguageLevel level) { 53 | myName = name; 54 | myLevel = level; 55 | } 56 | 57 | @Override 58 | public Sdk getSdk() { 59 | return myName == null 60 | ? PythonMockSdk.create(myLevel, getAdditionalRoots()) 61 | : PythonMockSdk.create(PythonHelpersLocator.getPythonCommunityPath() + "/testData" + "/" + myName); 62 | } 63 | 64 | /** 65 | * @return additional roots to add to mock python 66 | * @apiNote ignored when name is provided. 67 | */ 68 | protected VirtualFile @NotNull [] getAdditionalRoots() { 69 | return VirtualFile.EMPTY_ARRAY; 70 | } 71 | 72 | protected void createLibrary(ModifiableRootModel model, final String name, final String path) { 73 | final Library.ModifiableModel modifiableModel = model.getModuleLibraryTable().createLibrary(name).getModifiableModel(); 74 | final VirtualFile home = 75 | LocalFileSystem.getInstance().refreshAndFindFileByPath(PathManager.getHomePath() + path); 76 | 77 | modifiableModel.addRoot(home, OrderRootType.CLASSES); 78 | modifiableModel.commit(); 79 | } 80 | } -------------------------------------------------------------------------------- /intellij/src/test/java/com/jetbrains/python/fixtures/PyTestCase.java: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | package com.jetbrains.python.fixtures; 3 | 4 | import com.google.common.base.Joiner; 5 | import com.google.common.collect.Lists; 6 | import com.intellij.application.options.CodeStyle; 7 | import com.intellij.codeInsight.lookup.LookupElement; 8 | import com.intellij.codeInsight.lookup.LookupEx; 9 | import com.intellij.find.findUsages.CustomUsageSearcher; 10 | import com.intellij.find.findUsages.FindUsagesOptions; 11 | import com.intellij.openapi.actionSystem.IdeActions; 12 | import com.intellij.openapi.application.ApplicationManager; 13 | import com.intellij.openapi.application.WriteAction; 14 | import com.intellij.openapi.command.CommandProcessor; 15 | import com.intellij.openapi.command.WriteCommandAction; 16 | import com.intellij.openapi.editor.Editor; 17 | import com.intellij.openapi.editor.ex.EditorEx; 18 | import com.intellij.openapi.module.Module; 19 | import com.intellij.openapi.projectRoots.Sdk; 20 | import com.intellij.openapi.projectRoots.SdkModificator; 21 | import com.intellij.openapi.roots.OrderRootType; 22 | import com.intellij.openapi.roots.impl.FilePropertyPusher; 23 | import com.intellij.openapi.util.Disposer; 24 | import com.intellij.openapi.util.TextRange; 25 | import com.intellij.openapi.util.text.StringUtil; 26 | import com.intellij.openapi.vfs.LocalFileSystem; 27 | import com.intellij.openapi.vfs.StandardFileSystems; 28 | import com.intellij.openapi.vfs.VfsUtil; 29 | import com.intellij.openapi.vfs.VirtualFile; 30 | import com.intellij.psi.*; 31 | import com.intellij.psi.codeStyle.CodeStyleManager; 32 | import com.intellij.psi.codeStyle.CodeStyleSettings; 33 | import com.intellij.psi.codeStyle.CommonCodeStyleSettings; 34 | import com.intellij.psi.search.searches.ReferencesSearch; 35 | import com.intellij.refactoring.RefactoringActionHandler; 36 | import com.intellij.testFramework.*; 37 | import com.intellij.testFramework.fixtures.*; 38 | import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl; 39 | import com.intellij.usageView.UsageInfo; 40 | import com.intellij.usages.Usage; 41 | import com.intellij.usages.rules.PsiElementUsage; 42 | import com.intellij.util.CommonProcessors.CollectProcessor; 43 | import com.intellij.util.ContentsUtil; 44 | import com.intellij.util.IncorrectOperationException; 45 | import com.intellij.util.containers.ContainerUtil; 46 | import com.jetbrains.python.PythonHelpersLocator; 47 | import com.jetbrains.python.PythonLanguage; 48 | import com.jetbrains.python.codeInsight.completion.PyModuleNameCompletionContributor; 49 | import com.jetbrains.python.documentation.PyDocumentationSettings; 50 | import com.jetbrains.python.documentation.PythonDocumentationProvider; 51 | import com.jetbrains.python.documentation.docstrings.DocStringFormat; 52 | import com.jetbrains.python.namespacePackages.PyNamespacePackagesService; 53 | import com.jetbrains.python.psi.*; 54 | import com.jetbrains.python.psi.impl.PyFileImpl; 55 | import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; 56 | import com.jetbrains.python.psi.search.PySearchUtilBase; 57 | import com.jetbrains.python.psi.types.PyType; 58 | import com.jetbrains.python.psi.types.TypeEvalContext; 59 | import com.jetbrains.python.sdk.PythonSdkUtil; 60 | import org.jetbrains.annotations.NotNull; 61 | import org.jetbrains.annotations.Nullable; 62 | import org.junit.Assert; 63 | 64 | import java.io.File; 65 | import java.util.*; 66 | import java.util.function.Consumer; 67 | 68 | 69 | @TestDataPath("$CONTENT_ROOT/../testData/") 70 | public abstract class PyTestCase extends UsefulTestCase { 71 | 72 | protected static final PyLightProjectDescriptor ourPy2Descriptor = new PyLightProjectDescriptor(LanguageLevel.PYTHON27); 73 | protected static final PyLightProjectDescriptor ourPyLatestDescriptor = new PyLightProjectDescriptor(LanguageLevel.getLatest()); 74 | 75 | protected CodeInsightTestFixture myFixture; 76 | 77 | protected void assertProjectFilesNotParsed(@NotNull PsiFile currentFile) { 78 | assertRootNotParsed(currentFile, myFixture.getTempDirFixture().getFile("."), null); 79 | } 80 | 81 | protected void assertProjectFilesNotParsed(@NotNull TypeEvalContext context) { 82 | assertRootNotParsed(context.getOrigin(), myFixture.getTempDirFixture().getFile("."), context); 83 | } 84 | 85 | protected void assertSdkRootsNotParsed(@NotNull PsiFile currentFile) { 86 | final Sdk testSdk = PythonSdkUtil.findPythonSdk(currentFile); 87 | for (VirtualFile root : testSdk.getRootProvider().getFiles(OrderRootType.CLASSES)) { 88 | assertRootNotParsed(currentFile, root, null); 89 | } 90 | } 91 | 92 | private void assertRootNotParsed(@NotNull PsiFile currentFile, @NotNull VirtualFile root, @Nullable TypeEvalContext context) { 93 | for (VirtualFile file : VfsUtil.collectChildrenRecursively(root)) { 94 | final PyFile pyFile = PyUtil.as(myFixture.getPsiManager().findFile(file), PyFile.class); 95 | if (pyFile != null && !pyFile.equals(currentFile) && (context == null || !context.maySwitchToAST(pyFile))) { 96 | assertNotParsed(pyFile); 97 | } 98 | } 99 | } 100 | 101 | @Nullable 102 | protected static VirtualFile getVirtualFileByName(String fileName) { 103 | final VirtualFile path = LocalFileSystem.getInstance().findFileByPath(fileName.replace(File.separatorChar, '/')); 104 | if (path != null) { 105 | refreshRecursively(path); 106 | return path; 107 | } 108 | return null; 109 | } 110 | 111 | /** 112 | * Reformats currently configured file. 113 | */ 114 | protected final void reformatFile() { 115 | WriteCommandAction.runWriteCommandAction(null, () -> doPerformFormatting()); 116 | } 117 | 118 | private void doPerformFormatting() throws IncorrectOperationException { 119 | final PsiFile file = myFixture.getFile(); 120 | final TextRange myTextRange = file.getTextRange(); 121 | CodeStyleManager.getInstance(myFixture.getProject()).reformatText(file, myTextRange.getStartOffset(), myTextRange.getEndOffset()); 122 | } 123 | 124 | @Override 125 | protected void setUp() throws Exception { 126 | super.setUp(); 127 | IdeaTestFixtureFactory factory = IdeaTestFixtureFactory.getFixtureFactory(); 128 | TestFixtureBuilder fixtureBuilder = factory.createLightFixtureBuilder(getProjectDescriptor(), getTestName(false)); 129 | final IdeaProjectTestFixture fixture = fixtureBuilder.getFixture(); 130 | myFixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixture, createTempDirFixture()); 131 | myFixture.setTestDataPath(getTestDataPath()); 132 | myFixture.setUp(); 133 | } 134 | 135 | /** 136 | * @return fixture to be used as temporary dir. 137 | */ 138 | @NotNull 139 | protected TempDirTestFixture createTempDirFixture() { 140 | return new LightTempDirTestFixtureImpl(true); // "tmp://" dir by default 141 | } 142 | 143 | protected void runWithAdditionalFileInLibDir(@NotNull String relativePath, 144 | @NotNull String text, 145 | @NotNull Consumer fileConsumer) { 146 | final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule()); 147 | final VirtualFile libDir = PySearchUtilBase.findLibDir(sdk); 148 | if (libDir != null) { 149 | runWithAdditionalFileIn(relativePath, text, libDir, fileConsumer); 150 | } 151 | else { 152 | createAdditionalRootAndRunWithIt( 153 | sdk, 154 | "Lib", 155 | OrderRootType.CLASSES, 156 | root -> runWithAdditionalFileIn(relativePath, text, root, fileConsumer) 157 | ); 158 | } 159 | } 160 | 161 | protected void runWithAdditionalFileInSkeletonDir(@NotNull String relativePath, 162 | @NotNull String text, 163 | @NotNull Consumer fileConsumer) { 164 | final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule()); 165 | final VirtualFile skeletonsDir = PythonSdkUtil.findSkeletonsDir(sdk); 166 | if (skeletonsDir != null) { 167 | runWithAdditionalFileIn(relativePath, text, skeletonsDir, fileConsumer); 168 | } 169 | else { 170 | createAdditionalRootAndRunWithIt( 171 | sdk, 172 | PythonSdkUtil.SKELETON_DIR_NAME, 173 | PythonSdkUtil.BUILTIN_ROOT_TYPE, 174 | root -> runWithAdditionalFileIn(relativePath, text, root, fileConsumer) 175 | ); 176 | } 177 | } 178 | 179 | private static void runWithAdditionalFileIn(@NotNull String relativePath, 180 | @NotNull String text, 181 | @NotNull VirtualFile dir, 182 | @NotNull Consumer fileConsumer) { 183 | final VirtualFile file = VfsTestUtil.createFile(dir, relativePath, text); 184 | try { 185 | fileConsumer.accept(file); 186 | } 187 | finally { 188 | VfsTestUtil.deleteFile(file); 189 | } 190 | } 191 | 192 | protected void runWithAdditionalClassEntryInSdkRoots(@NotNull VirtualFile directory, @NotNull Runnable runnable) { 193 | final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule()); 194 | assertNotNull(sdk); 195 | runWithAdditionalRoot(sdk, directory, OrderRootType.CLASSES, (__) -> runnable.run()); 196 | } 197 | 198 | protected void runWithAdditionalClassEntryInSdkRoots(@NotNull String relativeTestDataPath, @NotNull Runnable runnable) { 199 | final String absPath = getTestDataPath() + "/" + relativeTestDataPath; 200 | final VirtualFile testDataDir = StandardFileSystems.local().findFileByPath(absPath); 201 | assertNotNull("Additional class entry directory '" + absPath + "' not found", testDataDir); 202 | runWithAdditionalClassEntryInSdkRoots(testDataDir, runnable); 203 | } 204 | 205 | private static void createAdditionalRootAndRunWithIt(@NotNull Sdk sdk, 206 | @NotNull String rootRelativePath, 207 | @NotNull OrderRootType rootType, 208 | @NotNull Consumer rootConsumer) { 209 | final VirtualFile tempRoot = VfsTestUtil.createDir(sdk.getHomeDirectory().getParent().getParent(), rootRelativePath); 210 | try { 211 | runWithAdditionalRoot(sdk, tempRoot, rootType, rootConsumer); 212 | } 213 | finally { 214 | VfsTestUtil.deleteFile(tempRoot); 215 | } 216 | } 217 | 218 | private static void runWithAdditionalRoot(@NotNull Sdk sdk, 219 | @NotNull VirtualFile root, 220 | @NotNull OrderRootType rootType, 221 | @NotNull Consumer rootConsumer) { 222 | WriteAction.run(() -> { 223 | final SdkModificator modificator = sdk.getSdkModificator(); 224 | assertNotNull(modificator); 225 | modificator.addRoot(root, rootType); 226 | modificator.commitChanges(); 227 | }); 228 | try { 229 | rootConsumer.accept(root); 230 | } 231 | finally { 232 | WriteAction.run(() -> { 233 | final SdkModificator modificator = sdk.getSdkModificator(); 234 | assertNotNull(modificator); 235 | modificator.removeRoot(root, rootType); 236 | modificator.commitChanges(); 237 | }); 238 | } 239 | } 240 | 241 | protected String getTestDataPath() { 242 | return PythonHelpersLocator.getPythonCommunityPath() + "/testData"; 243 | } 244 | 245 | @Override 246 | protected void tearDown() throws Exception { 247 | try { 248 | PyNamespacePackagesService.getInstance(myFixture.getModule()).resetAllNamespacePackages(); 249 | PyModuleNameCompletionContributor.ENABLED = true; 250 | setLanguageLevel(null); 251 | myFixture.tearDown(); 252 | myFixture = null; 253 | FilePropertyPusher.EP_NAME.findExtensionOrFail(PythonLanguageLevelPusher.class).flushLanguageLevelCache(); 254 | } 255 | catch (Throwable e) { 256 | addSuppressedException(e); 257 | } 258 | finally { 259 | super.tearDown(); 260 | } 261 | } 262 | 263 | @Nullable 264 | protected LightProjectDescriptor getProjectDescriptor() { 265 | return ourPyLatestDescriptor; 266 | } 267 | 268 | @Nullable 269 | protected PsiReference findReferenceBySignature(final String signature) { 270 | int pos = findPosBySignature(signature); 271 | return findReferenceAt(pos); 272 | } 273 | 274 | @Nullable 275 | protected PsiReference findReferenceAt(int pos) { 276 | return myFixture.getFile().findReferenceAt(pos); 277 | } 278 | 279 | protected int findPosBySignature(String signature) { 280 | return PsiDocumentManager.getInstance(myFixture.getProject()).getDocument(myFixture.getFile()).getText().indexOf(signature); 281 | } 282 | 283 | private void setLanguageLevel(@Nullable LanguageLevel languageLevel) { 284 | PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), languageLevel); 285 | } 286 | 287 | protected void runWithLanguageLevel(@NotNull LanguageLevel languageLevel, @NotNull Runnable runnable) { 288 | setLanguageLevel(languageLevel); 289 | try { 290 | runnable.run(); 291 | } 292 | finally { 293 | setLanguageLevel(null); 294 | } 295 | } 296 | 297 | protected void runWithDocStringFormat(@NotNull DocStringFormat format, @NotNull Runnable runnable) { 298 | final PyDocumentationSettings settings = PyDocumentationSettings.getInstance(myFixture.getModule()); 299 | final DocStringFormat oldFormat = settings.getFormat(); 300 | settings.setFormat(format); 301 | try { 302 | runnable.run(); 303 | } 304 | finally { 305 | settings.setFormat(oldFormat); 306 | } 307 | } 308 | 309 | protected void runWithSourceRoots(@NotNull List sourceRoots, @NotNull Runnable runnable) { 310 | final Module module = myFixture.getModule(); 311 | sourceRoots.forEach(root -> PsiTestUtil.addSourceRoot(module, root)); 312 | try { 313 | runnable.run(); 314 | } 315 | finally { 316 | sourceRoots.forEach(root -> PsiTestUtil.removeSourceRoot(module, root)); 317 | } 318 | } 319 | 320 | protected static void assertNotParsed(PsiFile file) { 321 | assertInstanceOf(file, PyFileImpl.class); 322 | assertNull("Operations should have been performed on stubs but caused file to be parsed: " + file.getVirtualFile().getPath(), 323 | ((PyFileImpl)file).getTreeElement()); 324 | } 325 | 326 | /** 327 | * @return class by its name from file 328 | */ 329 | @NotNull 330 | protected PyClass getClassByName(@NotNull final String name) { 331 | return myFixture.findElementByText("class " + name, PyClass.class); 332 | } 333 | 334 | /** 335 | * @see #moveByText(com.intellij.testFramework.fixtures.CodeInsightTestFixture, String) 336 | */ 337 | protected void moveByText(@NotNull final String testToFind) { 338 | moveByText(myFixture, testToFind); 339 | } 340 | 341 | /** 342 | * Finds some text and moves cursor to it (if found) 343 | * 344 | * @param fixture test fixture 345 | * @param testToFind text to find 346 | * @throws AssertionError if element not found 347 | */ 348 | public static void moveByText(@NotNull final CodeInsightTestFixture fixture, @NotNull final String testToFind) { 349 | final PsiElement element = fixture.findElementByText(testToFind, PsiElement.class); 350 | assert element != null : "No element found by text: " + testToFind; 351 | fixture.getEditor().getCaretModel().moveToOffset(element.getTextOffset()); 352 | } 353 | 354 | /** 355 | * Finds all usages of element. Works much like method in {@link com.intellij.testFramework.fixtures.CodeInsightTestFixture#findUsages(com.intellij.psi.PsiElement)}, 356 | * but supports {@link com.intellij.find.findUsages.CustomUsageSearcher} and {@link com.intellij.psi.search.searches.ReferencesSearch} as well 357 | * 358 | * @param element what to find 359 | * @return usages 360 | */ 361 | @NotNull 362 | protected Collection findUsage(@NotNull final PsiElement element) { 363 | final Collection result = new ArrayList<>(); 364 | final CollectProcessor usageCollector = new CollectProcessor<>(); 365 | for (final CustomUsageSearcher searcher : CustomUsageSearcher.EP_NAME.getExtensions()) { 366 | searcher.processElementUsages(element, usageCollector, new FindUsagesOptions(myFixture.getProject())); 367 | } 368 | for (final Usage usage : usageCollector.getResults()) { 369 | if (usage instanceof PsiElementUsage) { 370 | result.add(((PsiElementUsage)usage).getElement()); 371 | } 372 | } 373 | for (final PsiReference reference : ReferencesSearch.search(element).findAll()) { 374 | result.add(reference.getElement()); 375 | } 376 | 377 | for (final UsageInfo info : myFixture.findUsages(element)) { 378 | result.add(info.getElement()); 379 | } 380 | 381 | return result; 382 | } 383 | 384 | /** 385 | * Returns elements certain element allows to navigate to (emulates CTRL+Click, actually). 386 | * You need to pass element as argument or 387 | * make sure your fixture is configured for some element (see {@link com.intellij.testFramework.fixtures.CodeInsightTestFixture#getElementAtCaret()}) 388 | * 389 | * @param element element to fetch navigate elements from (may be null: element under caret would be used in this case) 390 | * @return elements to navigate to 391 | */ 392 | @NotNull 393 | protected Set getElementsToNavigate(@Nullable final PsiElement element) { 394 | final Set result = new HashSet<>(); 395 | final PsiElement elementToProcess = ((element != null) ? element : myFixture.getElementAtCaret()); 396 | for (final PsiReference reference : elementToProcess.getReferences()) { 397 | final PsiElement directResolve = reference.resolve(); 398 | if (directResolve != null) { 399 | result.add(directResolve); 400 | } 401 | if (reference instanceof PsiPolyVariantReference) { 402 | for (final ResolveResult resolveResult : ((PsiPolyVariantReference)reference).multiResolve(true)) { 403 | result.add(resolveResult.getElement()); 404 | } 405 | } 406 | } 407 | return result; 408 | } 409 | 410 | /** 411 | * Clears provided file 412 | * 413 | * @param file file to clear 414 | */ 415 | protected void clearFile(@NotNull final PsiFile file) { 416 | CommandProcessor.getInstance().executeCommand(myFixture.getProject(), () -> ApplicationManager.getApplication().runWriteAction(() -> { 417 | for (final PsiElement element : file.getChildren()) { 418 | element.delete(); 419 | } 420 | }), null, null); 421 | } 422 | 423 | /** 424 | * Runs refactoring using special handler 425 | * 426 | * @param handler handler to be used 427 | */ 428 | protected void refactorUsingHandler(@NotNull final RefactoringActionHandler handler) { 429 | final Editor editor = myFixture.getEditor(); 430 | assertInstanceOf(editor, EditorEx.class); 431 | handler.invoke(myFixture.getProject(), editor, myFixture.getFile(), ((EditorEx)editor).getDataContext()); 432 | } 433 | 434 | /** 435 | * Compares sets with string sorting them and displaying one-per-line to make comparision easier 436 | * 437 | * @param message message to display in case of error 438 | * @param actual actual set 439 | * @param expected expected set 440 | */ 441 | protected static void compareStringSets(@NotNull final String message, 442 | @NotNull final Set actual, 443 | @NotNull final Set expected) { 444 | final Joiner joiner = Joiner.on("\n"); 445 | Assert.assertEquals(message, joiner.join(new TreeSet<>(actual)), joiner.join(new TreeSet<>(expected))); 446 | } 447 | 448 | 449 | /** 450 | * Clicks certain button in document on caret position 451 | * 452 | * @param action what button to click (const from {@link IdeActions}) (btw, there should be some way to express it using annotations) 453 | * @see IdeActions 454 | */ 455 | protected final void pressButton(@NotNull final String action) { 456 | CommandProcessor.getInstance().executeCommand(myFixture.getProject(), () -> myFixture.performEditorAction(action), "", null); 457 | } 458 | 459 | @NotNull 460 | protected CommonCodeStyleSettings getCommonCodeStyleSettings() { 461 | return getCodeStyleSettings().getCommonSettings(PythonLanguage.getInstance()); 462 | } 463 | 464 | @NotNull 465 | protected CodeStyleSettings getCodeStyleSettings() { 466 | return CodeStyle.getSettings(myFixture.getProject()); 467 | } 468 | 469 | @NotNull 470 | protected CommonCodeStyleSettings.IndentOptions getIndentOptions() { 471 | return getCommonCodeStyleSettings().getIndentOptions(); 472 | } 473 | 474 | /** 475 | * When you have more than one completion variant, you may use this method providing variant to choose. 476 | * It only works for one caret (multiple carets not supported) and since it puts tab after completion, be sure to limit 477 | * line somehow (i.e. with comment). 478 | *
479 | * Example: "user.n[caret]." There are "name" and "nose" fields. 480 | * By calling this function with "nose" you will end with "user.nose ". 481 | */ 482 | protected final void completeCaretWithMultipleVariants(final String @NotNull ... desiredVariants) { 483 | final LookupElement[] lookupElements = myFixture.completeBasic(); 484 | final LookupEx lookup = myFixture.getLookup(); 485 | if (lookupElements != null && lookupElements.length > 1) { 486 | // More than one element returned, check directly because completion can't work in this case 487 | for (final LookupElement element : lookupElements) { 488 | final String suggestedString = element.getLookupString(); 489 | if (Arrays.asList(desiredVariants).contains(suggestedString)) { 490 | myFixture.getLookup().setCurrentItem(element); 491 | lookup.setCurrentItem(element); 492 | myFixture.completeBasicAllCarets('\t'); 493 | return; 494 | } 495 | } 496 | } 497 | } 498 | 499 | @NotNull 500 | protected PsiElement getElementAtCaret() { 501 | final PsiFile file = myFixture.getFile(); 502 | assertNotNull(file); 503 | return file.findElementAt(myFixture.getCaretOffset()); 504 | } 505 | 506 | public static void assertType(@NotNull String expectedType, @NotNull PyTypedElement element, @NotNull TypeEvalContext context) { 507 | assertType("Failed in " + context + " context", expectedType, element, context); 508 | } 509 | 510 | public static void assertType(@NotNull String message, 511 | @NotNull String expectedType, 512 | @NotNull PyTypedElement element, 513 | @NotNull TypeEvalContext context) { 514 | final PyType actual = context.getType(element); 515 | final String actualType = PythonDocumentationProvider.getTypeName(actual, context); 516 | assertEquals(message, expectedType, actualType); 517 | } 518 | 519 | public void addExcludedRoot(String rootPath) { 520 | final VirtualFile dir = myFixture.findFileInTempDir(rootPath); 521 | final Module module = myFixture.getModule(); 522 | assertNotNull(dir); 523 | PsiTestUtil.addExcludedRoot(module, dir); 524 | Disposer.register(myFixture.getProjectDisposable(), () -> PsiTestUtil.removeExcludedRoot(module, dir)); 525 | } 526 | 527 | public void assertContainsInRelativeOrder(@NotNull final Iterable actual, final T @Nullable ... expected) { 528 | final List actualList = Lists.newArrayList(actual); 529 | if (expected.length > 0) { 530 | T prev = expected[0]; 531 | int prevIndex = actualList.indexOf(prev); 532 | assertTrue(prev + " is not found in " + actualList, prevIndex >= 0); 533 | for (int i = 1; i < expected.length; i++) { 534 | final T next = expected[i]; 535 | final int nextIndex = actualList.indexOf(next); 536 | assertTrue(next + " is not found in " + actualList, nextIndex >= 0); 537 | assertTrue(prev + " should precede " + next + " in " + actualList, prevIndex < nextIndex); 538 | prev = next; 539 | prevIndex = nextIndex; 540 | } 541 | } 542 | } 543 | } -------------------------------------------------------------------------------- /intellij/src/test/kotlin/org/basedsoft/plugins/basedtyping/TestBasedTypeProvider.kt: -------------------------------------------------------------------------------- 1 | package org.basedsoft.plugins.basedtyping 2 | 3 | import com.jetbrains.python.PythonFileType 4 | import com.jetbrains.python.documentation.PythonDocumentationProvider 5 | import com.jetbrains.python.fixtures.PyTestCase 6 | import com.jetbrains.python.psi.PyExpression 7 | import com.jetbrains.python.psi.PyTypedElement 8 | import com.jetbrains.python.psi.types.TypeEvalContext 9 | 10 | 11 | class PyTypeProviderTest : PyTestCase() { 12 | fun `test bare literal`() { 13 | "expr: 1 | 2" exprIs "Literal[1, 2]" 14 | } 15 | 16 | fun `test tuple literal`() { 17 | "expr: (int, str)" exprIs "tuple[int, str]" 18 | } 19 | 20 | fun `test intersection`() { 21 | "expr: int & str" exprIs "int" 22 | } 23 | 24 | fun `test callable`() { 25 | """ 26 | expr: "(int, str) -> str" 27 | """.trimMargin() exprIs "(int, str) -> str" 28 | } 29 | 30 | fun `test infer overload`() { 31 | """ 32 | from typing import overload 33 | @overload 34 | def f(a: int) -> str: ... 35 | @overload 36 | def f(a: str) -> int: ... 37 | def f(a): 38 | expr = a 39 | """ exprIs "str | int" 40 | """ 41 | from typing import overload 42 | @overload 43 | def f(a: int) -> str: ... 44 | @overload 45 | def f(a: str) -> int: ... 46 | def f(a): 47 | return 1 48 | expr = f 49 | """ exprIs "(a: str | int) -> str | int" 50 | } 51 | 52 | fun `test constructor`() { 53 | """ 54 | class A: 55 | def __init__(self) -> None: ... 56 | expr = A() 57 | """ exprIs "A" 58 | } 59 | 60 | fun `test narrowing`() { 61 | """ 62 | a: object 63 | if isinstance(a, int): 64 | expr = a 65 | """ exprIs "int" 66 | } 67 | 68 | fun `test Final`() { 69 | """ 70 | from typing import Final 71 | expr: Final = 1 72 | """ exprIs "int" 73 | } 74 | 75 | private infix fun String.exprIs(expectedType: String) { 76 | myFixture.configureByText(PythonFileType.INSTANCE, this.trimIndent()) 77 | val expr = myFixture.findElementByText("expr", PyExpression::class.java) 78 | assertExpressionType(expectedType, expr) 79 | } 80 | 81 | private fun assertExpressionType(expectedType: String, expr: PyExpression) { 82 | val project = expr.project 83 | val containingFile = expr.containingFile 84 | assertType(expectedType, expr, TypeEvalContext.codeAnalysis(project, containingFile)) 85 | assertProjectFilesNotParsed(containingFile) 86 | assertType(expectedType, expr, TypeEvalContext.userInitiated(project, containingFile)) 87 | } 88 | 89 | fun assertType( 90 | expectedType: String, 91 | element: PyTypedElement, 92 | context: TypeEvalContext, 93 | message: String? = "Failed in $context context", 94 | ) { 95 | val actual = context.getType(element) 96 | val actualType = PythonDocumentationProvider.getTypeName(actual, context) 97 | assertEquals(message, expectedType, actualType) 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "basedmypy" 5 | version = "2.7.0" 6 | description = "Based static typing for Python" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "basedmypy-2.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:283635cb57c917c7f88146e801005dc0034a74c25d48397bb65699e2359d1085"}, 11 | {file = "basedmypy-2.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5158709f8c04fdb0f62d97551ad75366f3838e5dd3db0ffd83c32c3b64867e7d"}, 12 | {file = "basedmypy-2.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d76a8ee5e755024dcfb4e1ec0309ae6f95a0330cd71151cbe956a6740da5bc8"}, 13 | {file = "basedmypy-2.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0794aa3281bae1e88922223da6da5db9c6e495694bdc65cfc03acd97d1a94602"}, 14 | {file = "basedmypy-2.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:58a75892e74a8679a54a18b8e5959ae8c1612e925ccb8ccc07a5e767c44eaf8d"}, 15 | {file = "basedmypy-2.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f7a467856b12fc5b8085f065eaa36e1c08ea4e048a11777f47715dc4a9108e39"}, 16 | {file = "basedmypy-2.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94988835eec3c886daeae717b5dc16befbd339b520bd3d1d005b562836530d21"}, 17 | {file = "basedmypy-2.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cab30d2bfbba4e8bbd36b7bc1ea8811b40a062796edc653a8e4fd0881d4899e6"}, 18 | {file = "basedmypy-2.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:40f2324c82e7befc94ad722fc3e0fe9362b7607b93d29ae7930831e30e22ff6a"}, 19 | {file = "basedmypy-2.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:9470812c9a977d832bee52ebdd9b76bd30cea8f37f42ab1ce509e871b79996d3"}, 20 | {file = "basedmypy-2.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:678fc29448b87c92df4c035fac2ae53a8e675a3b29d46d08d6328147f95b4832"}, 21 | {file = "basedmypy-2.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5a770fb4e4714ef359b766dc29f44212f562ca387883c6ee5997cad2f4b6a838"}, 22 | {file = "basedmypy-2.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91caccc79132091e62dce7d6927003dda5b6930deb50c18bdae2b19c22c1c924"}, 23 | {file = "basedmypy-2.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1e3f32699843071df47445e71d53495e4e120b2df6469cc7efb3837080b770ab"}, 24 | {file = "basedmypy-2.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:f7f0e7d2f5bd943a7f3dfa6b314e0684d38fd7665f560bcec0c4958d2dd710d1"}, 25 | {file = "basedmypy-2.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4269027ee62f6ed4ded804e523a69a71eaec888ced8b6bc94c05b902b4dc970f"}, 26 | {file = "basedmypy-2.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee773922a22a3aba0eb2ce8f9b8733e1b6086b7b75b885f887cff0f9746bf607"}, 27 | {file = "basedmypy-2.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa155eb6506c2caf7296e97eb57e3870bc65e271af2582f2bb245b49acd283b0"}, 28 | {file = "basedmypy-2.7.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f938bc49ebe8ae81c1091f1934ac014b03464ead3cf93893b69f5da98c3ffbde"}, 29 | {file = "basedmypy-2.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:0758a45dc099f57110b21fd480dae82d289620501acbbb2854db5d22d6dd7b2a"}, 30 | {file = "basedmypy-2.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:594205870794dc8f1f0b86654949d283edfa6bf1197bb9c6097bd6ea153e763a"}, 31 | {file = "basedmypy-2.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c9141b0b7230032f930db1f2e1e60b3aa0e4df379f574ce38be680ac330cb7a"}, 32 | {file = "basedmypy-2.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c777c29cdbbb8f522347a2505d76f3b71a7d209e4690d1593192235d2850d682"}, 33 | {file = "basedmypy-2.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e61e51db7c7ab335d88b43d703bd85638edeb59f1ecfbb9292cab03f7ba0d19b"}, 34 | {file = "basedmypy-2.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:34ddbd6cf0ec678a501d32423fbbfd0a9966cb2016184f3e4f9a66b05ccd0e94"}, 35 | {file = "basedmypy-2.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e256804e471d7591109a87f7eadb632fece7184987ab7f15b8c75db60e20ab81"}, 36 | {file = "basedmypy-2.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:73c195a9bb7ddb94dc8763c90fb04bae3f8d84531fba54ec1d2ce7d0bfbe4caa"}, 37 | {file = "basedmypy-2.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a9b1caecc78d5bb570a1bf17e5bdf7a48e4e374f9f5b272515e976077937162"}, 38 | {file = "basedmypy-2.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:388b458f2b8d2720506fa5cdb925470f089ecf373fc7eb9d8e491dca202418de"}, 39 | {file = "basedmypy-2.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:d42880ebb9b8da5b9aa526bfc74c2272c0014a9db5f71dee41e92c92f1863abf"}, 40 | {file = "basedmypy-2.7.0-py3-none-any.whl", hash = "sha256:59661555a2637e0ece7ac65aca774451d60c3a9c1bd2613a96a067418fc381a7"}, 41 | {file = "basedmypy-2.7.0.tar.gz", hash = "sha256:a8ff08c667d8ca06c6ab5acd574e683b557ce64671b2a0e0c2503979f1186411"}, 42 | ] 43 | 44 | [package.dependencies] 45 | basedtyping = ">=0.1.4" 46 | mypy-extensions = ">=1.0.0" 47 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 48 | typing-extensions = ">=4.6.0" 49 | 50 | [package.extras] 51 | dmypy = ["psutil (>=4.0)"] 52 | faster-cache = ["orjson"] 53 | install-types = ["pip"] 54 | mypyc = ["setuptools (>=50)"] 55 | reports = ["lxml"] 56 | 57 | [[package]] 58 | name = "colorama" 59 | version = "0.4.6" 60 | description = "Cross-platform colored terminal text." 61 | optional = false 62 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 63 | files = [ 64 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 65 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 66 | ] 67 | 68 | [[package]] 69 | name = "exceptiongroup" 70 | version = "1.2.2" 71 | description = "Backport of PEP 654 (exception groups)" 72 | optional = false 73 | python-versions = ">=3.7" 74 | files = [ 75 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 76 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 77 | ] 78 | 79 | [package.extras] 80 | test = ["pytest (>=6)"] 81 | 82 | [[package]] 83 | name = "iniconfig" 84 | version = "2.0.0" 85 | description = "brain-dead simple config-ini parsing" 86 | optional = false 87 | python-versions = ">=3.7" 88 | files = [ 89 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 90 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 91 | ] 92 | 93 | [[package]] 94 | name = "mypy-extensions" 95 | version = "1.0.0" 96 | description = "Type system extensions for programs checked with the mypy type checker." 97 | optional = false 98 | python-versions = ">=3.5" 99 | files = [ 100 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 101 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 102 | ] 103 | 104 | [[package]] 105 | name = "packaging" 106 | version = "24.1" 107 | description = "Core utilities for Python packages" 108 | optional = false 109 | python-versions = ">=3.8" 110 | files = [ 111 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 112 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 113 | ] 114 | 115 | [[package]] 116 | name = "pluggy" 117 | version = "1.5.0" 118 | description = "plugin and hook calling mechanisms for python" 119 | optional = false 120 | python-versions = ">=3.8" 121 | files = [ 122 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 123 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 124 | ] 125 | 126 | [package.extras] 127 | dev = ["pre-commit", "tox"] 128 | testing = ["pytest", "pytest-benchmark"] 129 | 130 | [[package]] 131 | name = "pytest" 132 | version = "8.3.2" 133 | description = "pytest: simple powerful testing with Python" 134 | optional = false 135 | python-versions = ">=3.8" 136 | files = [ 137 | {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, 138 | {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, 139 | ] 140 | 141 | [package.dependencies] 142 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 143 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 144 | iniconfig = "*" 145 | packaging = "*" 146 | pluggy = ">=1.5,<2" 147 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 148 | 149 | [package.extras] 150 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 151 | 152 | [[package]] 153 | name = "ruff" 154 | version = "0.2.2" 155 | description = "An extremely fast Python linter and code formatter, written in Rust." 156 | optional = false 157 | python-versions = ">=3.7" 158 | files = [ 159 | {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, 160 | {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, 161 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, 162 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, 163 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, 164 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, 165 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, 166 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, 167 | {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, 168 | {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, 169 | {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, 170 | {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, 171 | {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, 172 | {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, 173 | {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, 174 | {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, 175 | {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, 176 | ] 177 | 178 | [[package]] 179 | name = "tomli" 180 | version = "2.0.1" 181 | description = "A lil' TOML parser" 182 | optional = false 183 | python-versions = ">=3.7" 184 | files = [ 185 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 186 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 187 | ] 188 | 189 | [[package]] 190 | name = "typing-extensions" 191 | version = "4.12.2" 192 | description = "Backported and Experimental Type Hints for Python 3.8+" 193 | optional = false 194 | python-versions = ">=3.8" 195 | files = [ 196 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 197 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 198 | ] 199 | 200 | [metadata] 201 | lock-version = "2.0" 202 | python-versions = "^3.9" 203 | content-hash = "d4378014c1883091f0ae89ab8140b49d6d7aeb8afae5a06899c068747968ed78" 204 | -------------------------------------------------------------------------------- /pw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ################################################################################## 4 | # Pyprojectx wrapper script # 5 | # https://github.com/pyprojectx/pyprojectx # 6 | # # 7 | # Copyright (c) 2021 Ivo Houbrechts # 8 | # # 9 | # Licensed under the MIT license # 10 | ################################################################################## 11 | import argparse 12 | import os 13 | import subprocess 14 | import sys 15 | from pathlib import Path 16 | from venv import EnvBuilder 17 | 18 | VERSION = "2.0.8" 19 | 20 | PYPROJECTX_INSTALL_DIR_ENV_VAR = "PYPROJECTX_INSTALL_DIR" 21 | PYPROJECTX_PACKAGE_ENV_VAR = "PYPROJECTX_PACKAGE" 22 | PYPROJECT_TOML = "pyproject.toml" 23 | DEFAULT_INSTALL_DIR = ".pyprojectx" 24 | 25 | CYAN = "\033[96m" 26 | BLUE = "\033[94m" 27 | RED = "\033[91m" 28 | RESET = "\033[0m" 29 | if sys.platform.startswith("win"): 30 | os.system("color") 31 | 32 | 33 | def run(args): 34 | try: 35 | options = get_options(args) 36 | pyprojectx_script = ensure_pyprojectx(options) 37 | explicit_options = [] 38 | if not options.toml: 39 | explicit_options += ["--toml", str(options.toml_path)] 40 | if not options.install_dir: 41 | explicit_options += ["--install-dir", str(options.install_path)] 42 | 43 | subprocess.run([str(pyprojectx_script), *explicit_options, *args], check=True) 44 | except subprocess.CalledProcessError as e: 45 | raise SystemExit(e.returncode) from e 46 | 47 | 48 | def get_options(args): 49 | options = arg_parser().parse_args(args) 50 | options.install_path = Path( 51 | options.install_dir 52 | or os.environ.get(PYPROJECTX_INSTALL_DIR_ENV_VAR, Path(__file__).with_name(DEFAULT_INSTALL_DIR)) 53 | ) 54 | options.toml_path = Path(options.toml) if options.toml else Path(__file__).with_name(PYPROJECT_TOML) 55 | if os.environ.get(PYPROJECTX_PACKAGE_ENV_VAR): 56 | options.version = "development" 57 | options.pyprojectx_package = os.environ.get(PYPROJECTX_PACKAGE_ENV_VAR) 58 | else: 59 | options.version = VERSION 60 | options.pyprojectx_package = f"pyprojectx~={VERSION}" 61 | options.verbosity = 0 if options.quiet or not options.verbosity else options.verbosity 62 | return options 63 | 64 | 65 | def arg_parser(): 66 | parser = argparse.ArgumentParser( 67 | description="Execute commands or aliases defined in the [tool.pyprojectx] section of pyproject.toml. " 68 | "Use the -i or --info option to see available tools and aliases.", 69 | allow_abbrev=False, 70 | ) 71 | parser.add_argument("--version", action="version", version=VERSION) 72 | parser.add_argument( 73 | "--toml", 74 | "-t", 75 | action="store", 76 | help="The toml config file. Defaults to 'pyproject.toml' in the same directory as the pw script.", 77 | ) 78 | parser.add_argument( 79 | "--install-dir", 80 | action="store", 81 | help=f"The directory where all tools (including pyprojectx) are installed; defaults to the " 82 | f"{PYPROJECTX_INSTALL_DIR_ENV_VAR} environment value if set, else '.pyprojectx' " 83 | f"in the same directory as the invoked pw script", 84 | ) 85 | parser.add_argument( 86 | "--force-install", 87 | "-f", 88 | action="store_true", 89 | help="Force clean installation of the virtual environment used to run cmd, if any", 90 | ) 91 | parser.add_argument( 92 | "--install-context", 93 | action="store", 94 | metavar="tool-context", 95 | help="Install a tool context without actually running any command.", 96 | ) 97 | parser.add_argument( 98 | "--verbose", 99 | "-v", 100 | action="count", 101 | dest="verbosity", 102 | help="Give more output. This option is additive and can be used up to 2 times.", 103 | ) 104 | parser.add_argument( 105 | "--quiet", 106 | "-q", 107 | action="store_true", 108 | help="Suppress output", 109 | ) 110 | parser.add_argument( 111 | "--info", 112 | "-i", 113 | action="store_true", 114 | help="Show the configuration details of a command instead of running it. " 115 | "If no command is specified, a list with all available tools and aliases is shown.", 116 | ) 117 | parser.add_argument( 118 | "--add", 119 | action="store", 120 | metavar="[context:],...", 121 | help="Add one or more packages to a tool context. " 122 | "If no context is specified, the packages are added to the main context. " 123 | "Packages can be specified as in 'pip install', except that a ',' can't be used in the version specification.", 124 | ) 125 | parser.add_argument( 126 | "--lock", 127 | action="store_true", 128 | help="Write all dependencies of all tool contexts to 'pw.lock' to guarantee reproducible outcomes.", 129 | ) 130 | parser.add_argument( 131 | "--install-px", action="store_true", help="Install the px and pxg scripts in your home directory." 132 | ) 133 | parser.add_argument( 134 | "--upgrade", 135 | action="store_true", 136 | help="Print instructions to download the latest pyprojectx wrapper scripts.", 137 | ) 138 | parser.add_argument( 139 | "command", nargs=argparse.REMAINDER, help="The command/alias with optional arguments to execute." 140 | ) 141 | return parser 142 | 143 | 144 | def ensure_pyprojectx(options): 145 | env_builder = EnvBuilder(with_pip=True) 146 | venv_dir = options.install_path.joinpath( 147 | "pyprojectx", f"{options.version}-py{sys.version_info.major}.{sys.version_info.minor}" 148 | ) 149 | env_context = env_builder.ensure_directories(venv_dir) 150 | pyprojectx_script = Path(env_context.bin_path, "pyprojectx") 151 | pyprojectx_exe = Path(env_context.bin_path, "pyprojectx.exe") 152 | pip_cmd = [env_context.env_exe, "-m", "pip", "install"] 153 | 154 | if options.quiet: 155 | out = subprocess.DEVNULL 156 | pip_cmd.append("--quiet") 157 | else: 158 | out = sys.stderr 159 | 160 | if not pyprojectx_script.is_file() and not pyprojectx_exe.is_file(): 161 | if not options.quiet: 162 | print(f"{CYAN}creating pyprojectx venv in {BLUE}{venv_dir}{RESET}", file=sys.stderr) 163 | env_builder.create(venv_dir) 164 | subprocess.run( 165 | [*pip_cmd, "--upgrade", "pip"], 166 | stdout=out, 167 | check=True, 168 | ) 169 | 170 | if not options.quiet: 171 | print( 172 | f"{CYAN}installing pyprojectx {BLUE}{options.version}: {options.pyprojectx_package} {RESET}", 173 | file=sys.stderr, 174 | ) 175 | if options.version == "development": 176 | if not options.quiet: 177 | print( 178 | f"{RED}WARNING: {options.pyprojectx_package} is installed in editable mode{RESET}", 179 | file=sys.stderr, 180 | ) 181 | pip_cmd.append("-e") 182 | subprocess.run([*pip_cmd, options.pyprojectx_package], stdout=out, check=True) 183 | return pyprojectx_script 184 | 185 | 186 | if __name__ == "__main__": 187 | run(sys.argv[1:]) 188 | -------------------------------------------------------------------------------- /pw.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | python "%~dp0pw" %* 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | authors = [ 3 | "DetachHead ", 4 | "KotlinIsland ", 5 | ] 6 | description = "Utilities for basedmypy" 7 | name = "basedtyping" 8 | version = "0.1.10" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.9" 12 | typing_extensions = "^4.12.2" 13 | 14 | [tool.poetry.group.dev.dependencies] 15 | basedmypy = "^2.7" 16 | pytest = "^8" 17 | ruff = "~0.2.1" 18 | 19 | [build-system] 20 | build-backend = "poetry.core.masonry.api" 21 | requires = ["poetry-core>=1.0.8"] 22 | 23 | [tool.pyprojectx] 24 | main = ["poetry==1.7.1"] 25 | 26 | [tool.mypy] 27 | python_version = 3.9 28 | packages = ["basedtyping", "tests"] 29 | always_false = ["BASEDMYPY_TYPE_CHECKING"] 30 | 31 | [tool.ruff.format] 32 | skip-magic-trailing-comma = true 33 | 34 | [tool.pytest.ini_options] 35 | filterwarnings = "error" 36 | xfail_strict = true 37 | 38 | [tool.ruff] 39 | respect-gitignore = true 40 | line-length = 100 41 | 42 | [tool.ruff.lint] 43 | extend-select = ["ALL"] 44 | ignore = [ 45 | "ANN", # flake8-annotations (covered by pyright) 46 | "EM", # flake8-errmsg 47 | "FIX", # flake8-fixme 48 | "PLR0913", # Too many arguments to function call 49 | "PLR0912", # Too many branches 50 | "PLR0915", # Too many statements 51 | "PLR2004", # Magic value used in comparison 52 | "PLR1722", # Use `sys.exit()` instead of `exit` 53 | "PLW2901", # `for` loop variable overwritten by assignment target 54 | "PLE0605", # Invalid format for `__all__`, must be `tuple` or `list` (covered by mypy) 55 | "PLR0911", # Too many return statements 56 | "PLW0603", # Using the global statement is discouraged 57 | "PLC0105", # `TypeVar` name does not reflect its covariance 58 | "PLC0414", # Import alias does not rename original package (used by mypy for explicit re-export) 59 | "RUF013", # PEP 484 prohibits implicit Optional (covered by mypy) 60 | "RUF016", # Slice in indexed access to type (covered by mypy) 61 | "TRY002", # Create your own exception 62 | "TRY003", # Avoid specifying long messages outside the exception class 63 | "D10", # Missing docstring 64 | "D203", # 1 blank line required before class docstring 65 | "D205", # 1 blank line required between summary line and description 66 | "D209", # Multi-line docstring closing quotes should be on a separate line 67 | "D212", # Multi-line docstring summary should start at the first line 68 | "D213", # Multi-line docstring summary should start at the second line 69 | "D400", # First line should end with a period 70 | "D401", # First line should be in imperative mood 71 | "D403", # First word of the first line should be properly capitalized 72 | "D404", # First word of the docstring should not be `This` 73 | "D405", # Section name should be properly capitalized 74 | "D406", # Section name should end with a newline 75 | "D415", # First line should end with a period, question mark, or exclamation point 76 | "D418", # Function/Method decorated with @overload shouldn't contain a docstring (vscode supports it) 77 | "D413", # blank-line-after-last-section 78 | "PT013", # Found incorrect import of pytest, use simple import pytest instead (only for bad linters that can't check the qualname) 79 | "TD002", # Missing author in TODO 80 | "CPY001", # missing-copyright-notice 81 | "C901", # max-complexity 82 | "SLF001", # private-member-access (covered by pyright) 83 | "PLC2701", # import-private-name (covered by pyright) 84 | "UP006", # non-pep585-annotation (covered by pyright) 85 | "UP007", # non-pep604-annotation (covered by pyright) 86 | "UP035", # deprecated-import (covered by pyright) 87 | "ISC001", # single-line-implicit-string-concatenation (conflicts with formatter) 88 | "COM812", # missing-trailing-comma (conflicts with formatter) 89 | "ISC003", # explicit-string-concatenation (https://github.com/astral-sh/ruff/issues/9965) 90 | "N816", # clashes with based `in_` denotation 91 | ] 92 | 93 | [tool.ruff.lint.pycodestyle] 94 | ignore-overlong-task-comments = true 95 | 96 | [tool.ruff.lint.flake8-pytest-style] 97 | fixture-parentheses = false 98 | 99 | [tool.ruff.lint.per-file-ignores] 100 | "tests/*" = ["S"] # The tests don't need to be secure 101 | 102 | [tool.ruff.lint.isort] 103 | combine-as-imports = true 104 | required-imports = ["from __future__ import annotations"] 105 | split-on-trailing-comma = false 106 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_as_functiontype.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from basedtyping import as_functiontype 6 | 7 | 8 | def test_as_functiontype(): 9 | with pytest.raises(TypeError): 10 | as_functiontype(all) 11 | assert as_functiontype(test_as_functiontype) is test_as_functiontype 12 | -------------------------------------------------------------------------------- /tests/test_basedmypy_typechecking.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from basedtyping import BASEDMYPY_TYPE_CHECKING 4 | 5 | 6 | def _(): 7 | """type test""" 8 | if BASEDMYPY_TYPE_CHECKING: # noqa: SIM108 9 | _ = 1 + "" # type: ignore[operator] 10 | else: 11 | _ = 1 + "" 12 | -------------------------------------------------------------------------------- /tests/test_basedspecialform.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from basedtyping import Intersection, TypeForm, Untyped 4 | 5 | 6 | def test_basedgenericalias_intersection(): 7 | assert TypeForm[int] & None == Intersection[TypeForm[int], None] 8 | assert None & TypeForm[int] == Intersection[None, TypeForm[int]] 9 | 10 | 11 | def test_basedspecialform_intersection(): 12 | assert Untyped & None == Intersection[Untyped, None] 13 | assert None & Untyped == Intersection[Untyped, None] 14 | -------------------------------------------------------------------------------- /tests/test_function_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | # these are just type-time tests, not real life pytest tests. they are only run by mypy 7 | 8 | from basedtyping import AnyCallable 9 | from basedtyping.typetime_only import assert_type 10 | 11 | assert_function = assert_type[AnyCallable] 12 | 13 | def test_lambda_type(): 14 | assert_function(lambda: ...) 15 | 16 | def test_function_type(): 17 | def func(): 18 | pass 19 | 20 | assert_function(func) 21 | 22 | def test_builtin_function(): 23 | assert_function(len) 24 | 25 | def test_builtin_method(): 26 | assert_function([].append) 27 | 28 | def test_wrapper_descriptor(): 29 | assert_function(object.__init__) 30 | 31 | def test_method_wrapper(): 32 | assert_function(object().__str__) 33 | 34 | def test_method_descriptor(): 35 | assert_function(str.join) 36 | 37 | def test_class_method_descriptor(): 38 | assert_function(dict.fromkeys) 39 | -------------------------------------------------------------------------------- /tests/test_get_type_hints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | 5 | from typing_extensions import Literal, Union, override 6 | 7 | from basedtyping import get_type_hints 8 | 9 | 10 | def test_get_type_hints_class(): 11 | result: object = None 12 | 13 | class Base: 14 | @override 15 | def __init_subclass__(cls): 16 | nonlocal result 17 | result = get_type_hints(cls) 18 | 19 | class A(Base): 20 | a: A 21 | 22 | assert result == {"a": A} 23 | 24 | 25 | def test_get_type_hints_based(): 26 | class A: 27 | a: Union[re.RegexFlag.ASCII, re.RegexFlag.DOTALL] 28 | 29 | assert get_type_hints(A) == { 30 | "a": Union[Literal[re.RegexFlag.ASCII], Literal[re.RegexFlag.DOTALL]] # noqa: PYI030 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_intersection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pickle 4 | 5 | from basedtyping import Intersection 6 | 7 | 8 | class A: 9 | x: int 10 | 11 | 12 | class B: 13 | y: int 14 | 15 | 16 | class C(A, B): 17 | ... 18 | 19 | 20 | value = Intersection[A, B] 21 | other = Intersection[A, int] 22 | 23 | 24 | def test_intersection(): 25 | assert ( 26 | str(value) == f"basedtyping.Intersection[{A.__module__}.{A.__qualname__}," 27 | f" {B.__module__}.{B.__qualname__}]" 28 | ) 29 | 30 | 31 | def test_intersection_eq(): 32 | assert value == value # noqa: PLR0124 we are testing __eq__ 33 | assert value != other 34 | 35 | 36 | def test_intersection_eq_hash(): 37 | assert hash(value) == hash(value) 38 | assert hash(value) != other # type: ignore[comparison-overlap] 39 | 40 | 41 | def test_intersection_instancecheck(): 42 | assert isinstance(C(), value) # type: ignore[misc, arg-type, misc] 43 | assert not isinstance(A(), value) # type: ignore[misc, arg-type, misc] 44 | assert not isinstance(B(), value) # type: ignore[misc, arg-type, misc] 45 | 46 | 47 | def test_intersection_subclasscheck(): 48 | assert issubclass(C, value) # type: ignore[misc, arg-type, misc] 49 | assert not issubclass(A, value) # type: ignore[misc, arg-type, misc] 50 | assert not issubclass(B, value) # type: ignore[misc, arg-type, misc] 51 | 52 | 53 | def test_intersection_reduce(): 54 | pickled = pickle.dumps(value) 55 | loaded = pickle.loads(pickled) # type: ignore[no-any-expr] 56 | assert loaded is value # type: ignore[no-any-expr] 57 | assert loaded is not other # type: ignore[no-any-expr] 58 | -------------------------------------------------------------------------------- /tests/test_is_subform.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Union 5 | 6 | from basedtyping import issubform 7 | 8 | 9 | def test_normal(): 10 | if sys.version_info >= (3, 10): 11 | assert issubform(int, int | str) 12 | 13 | 14 | def test_union_first_arg(): 15 | if sys.version_info >= (3, 10): 16 | assert not issubform(int | str, int) 17 | assert issubform(int | str, object) 18 | assert issubform(int | str, int | str) 19 | assert issubform(int | str, Union[int, str]) # type: ignore[unused-ignore, arg-type] 20 | 21 | 22 | def test_old_union(): 23 | assert not issubform(Union[int, str], int) 24 | assert issubform(Union[int, str], object) 25 | assert issubform(Union[int, str], Union[str, int]) 26 | if sys.version_info >= (3, 10): 27 | assert issubform( 28 | Union[int, str], 29 | int | str, # type: ignore[unused-ignore, arg-type] 30 | ) 31 | -------------------------------------------------------------------------------- /tests/test_never_type/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/tests/test_never_type/__init__.py -------------------------------------------------------------------------------- /tests/test_never_type/test_runtime.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pytest import mark, raises 4 | from typing_extensions import Never 5 | 6 | from basedtyping import issubform 7 | 8 | # type ignores due to # https://github.com/KotlinIsland/basedmypy/issues/136 9 | 10 | 11 | @mark.xfail # https://github.com/KotlinIsland/basedtyping/issues/22 12 | def test_isinstance(): 13 | assert not isinstance(1, Never) # type: ignore[arg-type] 14 | 15 | 16 | def test_issubform_true(): 17 | assert issubform(Never, int) 18 | 19 | 20 | def test_issubform_false(): 21 | assert not issubform(str, Never) 22 | 23 | 24 | def test_issubform_never_is_never(): 25 | assert issubform(Never, Never) 26 | 27 | 28 | def test_issubclass(): 29 | with raises(TypeError): 30 | assert issubclass(int, Never) # type: ignore[arg-type] 31 | 32 | 33 | def test_cant_instantiate(): 34 | with raises(TypeError): 35 | Never() # type: ignore[operator] 36 | 37 | 38 | def test_cant_subtype(): 39 | with raises(TypeError): 40 | 41 | class _SubNever(Never): # type: ignore[misc] 42 | pass 43 | -------------------------------------------------------------------------------- /tests/test_never_type/test_typetime.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, NoReturn, cast 4 | 5 | if TYPE_CHECKING: 6 | # these are just type-time tests, not real life pytest tests. they are only run by mypy 7 | 8 | from typing_extensions import Never 9 | 10 | from basedtyping.typetime_only import assert_type 11 | 12 | def test_never_equals_noreturn(): 13 | # TODO: better way to check if types are equal 14 | # https://github.com/KotlinIsland/basedtyping/issues/33 15 | assert_type[NoReturn](cast(Never, 1)) 16 | assert_type[Never](cast(NoReturn, 1)) 17 | 18 | def test_valid_type_hint(): 19 | _never: Never 20 | 21 | def test_cant_assign_to_never(): 22 | _never: Never = 1 # type: ignore[assignment] 23 | 24 | def test_cant_subtype(): 25 | class _A(Never): # type: ignore[misc] 26 | ... 27 | 28 | def test_type_never(): 29 | """``type[Never]`` is invalid as ``Never`` is not a ``type``. 30 | 31 | Should actually be: 32 | ``_t: type[Never] # type: ignore[type-var]`` 33 | due to https://github.com/python/mypy/issues/11291 34 | 35 | So current implementation resembles an xfail. 36 | """ 37 | _t: type[Never] 38 | -------------------------------------------------------------------------------- /tests/test_reified_generics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/tests/test_reified_generics/__init__.py -------------------------------------------------------------------------------- /tests/test_reified_generics/test_identity.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Tuple, TypeVar 4 | 5 | from basedtyping import ReifiedGeneric, T 6 | 7 | U = TypeVar("U") 8 | 9 | 10 | class Reified(ReifiedGeneric[Tuple[T, U]]): 11 | pass 12 | 13 | 14 | def test_true() -> None: 15 | assert Reified[int, str] is Reified[int, str] 16 | 17 | 18 | def test_false() -> None: 19 | assert ( 20 | Reified[int, bool] is not Reified[int, str] # type: ignore[comparison-overlap] 21 | ) 22 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_isinstance.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Tuple, TypeVar 4 | 5 | from pytest import mark 6 | 7 | from basedtyping import ReifiedGeneric, T 8 | 9 | T2 = TypeVar("T2") 10 | 11 | 12 | class Reified(ReifiedGeneric[Tuple[T, T2]]): 13 | pass 14 | 15 | 16 | class Reified2(ReifiedGeneric[Tuple[T, T2]]): 17 | pass 18 | 19 | 20 | @mark.xfail(reason="not implemented") 21 | def test_isinstance_with_out_of_order_params() -> None: 22 | class A(ReifiedGeneric[T]): 23 | pass 24 | 25 | class B(ReifiedGeneric[T]): 26 | pass 27 | 28 | class C1(A[T], B[T2], ReifiedGeneric[Tuple[T, T2]]): 29 | pass 30 | 31 | class C2(A[T], B[T2], ReifiedGeneric[Tuple[T2, T]]): 32 | pass 33 | 34 | assert isinstance(C1[int, str](), B[str]) # type: ignore[misc] 35 | assert not isinstance(C1[str, int](), B[str]) # type: ignore[misc] 36 | assert issubclass(C1[int, str], B[str]) # type: ignore[misc] 37 | assert not issubclass(C1[str, int], B[str]) # type: ignore[misc] 38 | 39 | assert isinstance(C2[int, str](), B[str]) # type: ignore[misc] 40 | assert not isinstance(C2[str, int](), B[str]) # type: ignore[misc] 41 | assert issubclass(C2[int, str], B[str]) # type: ignore[misc] 42 | assert not issubclass(C2[str, int], B[str]) # type: ignore[misc] 43 | 44 | 45 | def test_isinstance() -> None: 46 | # https://github.com/KotlinIsland/basedmypy/issues/5 47 | assert isinstance(Reified[int, str](), Reified[int, str]) # type: ignore[misc] 48 | assert not isinstance(Reified[int, str](), Reified[int, int]) # type: ignore[misc] 49 | 50 | 51 | def test_without_generics_true() -> None: 52 | assert isinstance(Reified[int, str](), Reified) 53 | 54 | 55 | def test_without_generics_false() -> None: 56 | assert not isinstance(Reified[int, str](), Reified2) 57 | 58 | 59 | def test_without_generics_one_specified() -> None: 60 | class SubReified(Reified[int, T2]): 61 | pass 62 | 63 | assert isinstance(SubReified[str](), SubReified) 64 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_issubclass.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Tuple, TypeVar, Union 4 | 5 | from pytest import mark 6 | 7 | from basedtyping import ReifiedGeneric, T, out_T 8 | 9 | T2 = TypeVar("T2") 10 | 11 | 12 | class Reified(ReifiedGeneric[Tuple[T, T2]]): 13 | pass 14 | 15 | 16 | # https://github.com/KotlinIsland/basedmypy/issues/5 17 | 18 | 19 | def test_issubclass(): 20 | assert issubclass(Reified[int, str], Reified[int, str]) # type: ignore[misc] 21 | assert not issubclass(Reified[int, str], Reified[int, int]) # type: ignore[misc] 22 | 23 | 24 | def test_wrong_class_same_generics(): 25 | class Reified2(ReifiedGeneric[Tuple[T, T2]]): 26 | pass 27 | 28 | assert not issubclass(Reified2[int, int], Reified[int, int]) # type: ignore[misc] 29 | 30 | 31 | @mark.xfail(reason="not implemented") 32 | def test_without_generics_first_arg_false(): 33 | assert not issubclass(Reified, Reified[int, str]) # type: ignore[misc] 34 | 35 | 36 | @mark.xfail(reason="not implemented") 37 | def test_without_generics_first_arg_true(): 38 | # https://github.com/KotlinIsland/basedtyping/issues/70 39 | class Foo(ReifiedGeneric[out_T]): # type:ignore[type-var] 40 | pass 41 | 42 | assert not issubclass(Foo, Foo[object]) # type: ignore[misc] 43 | 44 | 45 | def test_without_generics_second_arg(): 46 | assert issubclass(Reified[int, str], Reified) 47 | 48 | 49 | def test_without_generics_both(): 50 | class SubReified(Reified[T, T2]): 51 | pass 52 | 53 | assert issubclass(SubReified, Reified) 54 | assert not issubclass(Reified, SubReified) 55 | 56 | 57 | @mark.xfail(reason="not implemented") 58 | def test_without_generics_same_as_bound(): 59 | _T = TypeVar("_T", bound=Union[int, str]) # noqa: PYI018 60 | 61 | class Foo(ReifiedGeneric[T]): 62 | pass 63 | 64 | assert issubclass(Foo, Foo[Union[int, str]]) # type: ignore[misc] 65 | assert issubclass(Foo[Union[int, str]], Foo) 66 | 67 | 68 | def test_without_generics_one_specified(): 69 | class SubReified(Reified[int, T2]): 70 | pass 71 | 72 | assert issubclass(SubReified[str], SubReified) 73 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_not_enough_type_params.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Tuple, TypeVar 4 | 5 | from pytest import raises 6 | 7 | from basedtyping import NotEnoughTypeParametersError, ReifiedGeneric, T 8 | 9 | U = TypeVar("U") 10 | 11 | 12 | class Reified2(ReifiedGeneric[Tuple[T, U]]): 13 | pass 14 | 15 | 16 | def test_not_enough_type_params(): 17 | with raises(NotEnoughTypeParametersError): 18 | Reified2[int]() # type: ignore[misc] 19 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_reified_generic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Generic, List, Tuple, TypeVar 4 | 5 | from pytest import raises 6 | 7 | from basedtyping import NotReifiedError, ReifiedGeneric, T 8 | 9 | T2 = TypeVar("T2") 10 | 11 | NoneType = type(None) 12 | 13 | 14 | class Reified(ReifiedGeneric[Tuple[T, T2]]): 15 | pass 16 | 17 | 18 | # TODO: investigate this "metaclass conflict" mypy error 19 | # https://github.com/KotlinIsland/basedtyping/issues/76 20 | class ReifiedList(ReifiedGeneric[Tuple[T]], List[T]): # type:ignore[misc] 21 | pass 22 | 23 | 24 | class Normal(Generic[T, T2]): 25 | pass 26 | 27 | 28 | not_reified_parameter_error_match = "TypeVars cannot be used" 29 | 30 | 31 | def test_class_args_and_params_class(): 32 | assert ( 33 | Normal[int, str].__args__ # type: ignore[attr-defined] 34 | == Reified[int, str].__reified_generics__ 35 | ) 36 | assert ( 37 | Normal[int, str].__parameters__ # type: ignore[attr-defined] 38 | == Reified[int, str].__type_vars__ 39 | ) 40 | 41 | 42 | def test_class_args_and_params_instance(): 43 | assert Reified[int, str]().__reified_generics__ == (int, str) 44 | assert not Reified[int, str]().__type_vars__ 45 | 46 | 47 | def test_reified_list(): 48 | it = ReifiedList[int]([1, 2, 3]) 49 | assert it.__reified_generics__ == (int,) 50 | assert not it.__type_vars__ 51 | 52 | 53 | def test_reified_generic_without_generic_alias(): 54 | with raises(NotReifiedError, match="Cannot instantiate ReifiedGeneric "): 55 | Reified() 56 | 57 | 58 | def test_reified_in_init(): 59 | class Foo(ReifiedGeneric[T]): 60 | def __init__(self): 61 | assert self.__reified_generics__ == (int,) 62 | 63 | Foo[int]() 64 | 65 | 66 | def test_concrete_subclass(): 67 | class A(ReifiedGeneric[T]): 68 | pass 69 | 70 | class SubASpecified(A[int]): 71 | pass 72 | 73 | assert issubclass(SubASpecified, A[int]) # type: ignore[misc] 74 | assert not issubclass(SubASpecified, A[str]) # type: ignore[misc] 75 | 76 | s = SubASpecified() 77 | assert isinstance(s, A[int]) # type: ignore[misc] 78 | assert not isinstance(s, A[str]) # type: ignore[misc] 79 | 80 | 81 | def test_none_type(): 82 | assert Reified[None, None].__reified_generics__ == (NoneType, NoneType) 83 | 84 | 85 | if TYPE_CHECKING: 86 | # this is just a type-time test, not a real life pytest test. it's only run by mypy 87 | from typing_extensions import assert_type 88 | 89 | def test_reified_generic_subtype_self(): 90 | """make sure that the generic in the metaclass doesn't break instance types, and that 91 | the `Self` type works properly on the metaclass""" 92 | 93 | class Subtype(Reified[int, int]): 94 | pass 95 | 96 | assert_type(Subtype(), Subtype) 97 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_subclasses.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, Tuple, TypeVar 4 | 5 | from pytest import raises 6 | 7 | from basedtyping import NotReifiedError, ReifiedGeneric, T 8 | 9 | T2 = TypeVar("T2") 10 | 11 | no_parameters_error_match = "Cannot instantiate ReifiedGeneric " 12 | 13 | 14 | class Reified(ReifiedGeneric[Tuple[T, T2]]): 15 | pass 16 | 17 | 18 | # TODO: investigate this "metaclass conflict" mypy error 19 | # https://github.com/KotlinIsland/basedtyping/issues/76 20 | class ReifiedList(ReifiedGeneric[Tuple[T]], List[T]): # type:ignore[misc] 21 | pass 22 | 23 | 24 | def test_subclass() -> None: 25 | class SubReified1(Reified[T, T2]): 26 | pass 27 | 28 | class SubReified2(Reified[T, T2], ReifiedGeneric[Tuple[T, T2]]): 29 | pass 30 | 31 | with raises(NotReifiedError, match=no_parameters_error_match): 32 | SubReified1() 33 | with raises(NotReifiedError, match=no_parameters_error_match): 34 | SubReified2() 35 | s = SubReified2[int, str]() 36 | assert isinstance(s, Reified[int, str]) # type: ignore[misc] 37 | assert not isinstance(s, Reified[str, int]) # type: ignore[misc] 38 | 39 | 40 | def test_subclass_one_generic_specified() -> None: 41 | class SubReified(Reified[int, T2]): 42 | pass 43 | 44 | assert SubReified[str]().__reified_generics__ == (int, str) 45 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_type_of_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Tuple, Type 4 | 5 | if TYPE_CHECKING: 6 | # these are just type-time tests, not real life pytest tests. They are only run by mypy 7 | # they are currently failing since it's not currently possible to maintain the 8 | # types of each type 9 | 10 | from typing import TypeVar 11 | 12 | from basedtyping import ReifiedGeneric, T 13 | 14 | U = TypeVar("U") 15 | 16 | class Reified(ReifiedGeneric[Tuple[T, U]]): 17 | pass 18 | 19 | from basedtyping.typetime_only import assert_type 20 | 21 | def test_instance(): 22 | """may be possible once https://github.com/KotlinIsland/basedmypy/issues/24 is resolved""" 23 | assert_type[Tuple[Type[int], Type[str]]]( 24 | Reified[int, str]().__reified_generics__ # type: ignore[arg-type] 25 | ) 26 | 27 | def from_class(): 28 | """may be possible once https://github.com/python/mypy/issues/11672 is resolved""" 29 | assert_type[Tuple[Type[int], Type[str]]]( 30 | Reified[int, str].__reified_generics__ # type: ignore[arg-type] 31 | ) 32 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_typevars.py: -------------------------------------------------------------------------------- 1 | """mypy should catch these, but it doesn't due to https://github.com/python/mypy/issues/7084""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Generic 6 | 7 | from pytest import raises 8 | 9 | from basedtyping import NotReifiedError, ReifiedGeneric, T 10 | 11 | 12 | class Reified(ReifiedGeneric[T]): 13 | pass 14 | 15 | 16 | class TestTypeVars(Generic[T]): 17 | def test_instantiate(self): 18 | with raises(NotReifiedError): 19 | Reified[T]() 20 | 21 | def test_isinstance(self): 22 | with raises(NotReifiedError): 23 | isinstance(Reified[str](), Reified[T]) # type: ignore[misc] 24 | 25 | def test_unbound_instantiate(self): 26 | with raises(NotReifiedError): 27 | Reified[T]() 28 | 29 | def test_unbound_isinstance(self): 30 | with raises(NotReifiedError): 31 | isinstance(Reified[str](), Reified[T]) # type: ignore[misc] 32 | 33 | def test_issubclass_left(self): 34 | with raises(NotReifiedError): 35 | issubclass(Reified[T], Reified[int]) # type: ignore[misc] 36 | 37 | def test_issubclass_right(self): 38 | with raises(NotReifiedError): 39 | issubclass(Reified[int], Reified[T]) # type: ignore[misc] 40 | -------------------------------------------------------------------------------- /tests/test_reified_generics/test_variance.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | from basedtyping import ReifiedGeneric, T, in_T, out_T 6 | 7 | # type ignores are due to https://github.com/KotlinIsland/basedmypy/issues/5 8 | 9 | 10 | def test_covariant(): 11 | # https://github.com/KotlinIsland/basedtyping/issues/70 12 | class Foo(ReifiedGeneric[out_T]): # type:ignore[type-var] 13 | pass 14 | 15 | assert isinstance(Foo[int](), Foo[Union[int, str]]) # type: ignore[misc] 16 | assert not isinstance(Foo[Union[int, str]](), Foo[int]) # type: ignore[misc] 17 | 18 | 19 | def test_contravariant(): 20 | # https://github.com/KotlinIsland/basedtyping/issues/70 21 | class Foo(ReifiedGeneric[in_T]): # type:ignore[type-var] 22 | pass 23 | 24 | assert isinstance(Foo[Union[int, str]](), Foo[int]) # type: ignore[misc] 25 | assert not isinstance(Foo[int](), Foo[Union[int, str]]) # type: ignore[misc] 26 | 27 | 28 | def test_invariant(): 29 | class Foo(ReifiedGeneric[T]): 30 | pass 31 | 32 | assert not isinstance(Foo[int](), Foo[Union[int, str]]) # type: ignore[misc] 33 | assert not isinstance(Foo[Union[int, str]](), Foo[int]) # type: ignore[misc] 34 | -------------------------------------------------------------------------------- /tests/test_runtime_only/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/tests/test_runtime_only/__init__.py -------------------------------------------------------------------------------- /tests/test_runtime_only/test_literal_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | 6 | def test_literal_type_positive(): 7 | from typing import Literal 8 | 9 | from basedtyping.runtime_only import LiteralType 10 | 11 | assert isinstance(Literal[1, 2], LiteralType) 12 | 13 | 14 | def test_literal_type_negative(): 15 | from basedtyping.runtime_only import LiteralType 16 | 17 | assert not isinstance(Union[int, str], LiteralType) 18 | -------------------------------------------------------------------------------- /tests/test_runtime_only/test_old_union_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Union 5 | 6 | from basedtyping.runtime_only import OldUnionType 7 | 8 | 9 | def test_old_union(): 10 | assert isinstance(Union[int, str], OldUnionType) 11 | 12 | 13 | def test_new_union(): 14 | if sys.version_info >= (3, 10): 15 | assert not isinstance(int | str, OldUnionType) 16 | -------------------------------------------------------------------------------- /tests/test_transformer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from enum import Enum 5 | from types import FunctionType # noqa: F401 6 | from typing import Dict, List, Tuple, cast 7 | from unittest import skipIf 8 | 9 | from pytest import raises 10 | from typing_extensions import Annotated, Callable, Literal, TypeGuard, TypeIs, Union 11 | 12 | from basedtyping import ForwardRef, Intersection 13 | from basedtyping.transformer import eval_type_based 14 | 15 | # ruff: noqa: PYI030 the unions of literals are an artifact of the implementation, they have no bearing on anything practical 16 | 17 | 18 | def validate(value: str, expected: object, *, string_literals=False): 19 | assert ( 20 | eval_type_based( 21 | ForwardRef(value), 22 | globalns=cast(Dict[str, object], globals()), 23 | string_literals=string_literals, 24 | ) 25 | == expected 26 | ) 27 | 28 | 29 | @skipIf(sys.version_info <= (3, 10), "unsupported") # type: ignore[no-any-expr] 30 | def test_literal(): 31 | validate("1 | 2", Union[Literal[1], Literal[2]]) 32 | 33 | 34 | def test_negative(): 35 | validate("-1", Literal[-1]) 36 | validate("+1", Literal[1]) 37 | 38 | 39 | def test_literal_union(): 40 | validate("Union[1, 2]", Union[Literal[1], Literal[2]]) 41 | 42 | 43 | def test_literal_literal(): 44 | validate("Literal[1]", Literal[1]) 45 | 46 | 47 | def test_literal_nested(): 48 | validate("(1, 2)", Tuple[Literal[1], Literal[2]]) 49 | validate("List[(1, 2),]", List[Tuple[Literal[1], Literal[2]]]) 50 | 51 | 52 | def test_literal_str_forwardref(): 53 | validate("'1'", Literal[1]) 54 | validate("Literal['1']", Literal["1"]) 55 | 56 | 57 | def test_literal_str_literal(): 58 | validate("'1'", Literal["1"], string_literals=True) 59 | validate("Literal['1']", Literal["1"], string_literals=True) 60 | 61 | 62 | class E(Enum): 63 | a = 1 64 | b = 2 65 | 66 | 67 | @skipIf(sys.version_info <= (3, 10), "unsupported") # type: ignore[no-any-expr] 68 | def test_literal_enum(): 69 | validate("E.a | E.b", Union[Literal[E.a], Literal[E.b]]) 70 | 71 | 72 | def test_literal_enum_union(): 73 | validate("Union[E.a, E.b]", Union[Literal[E.a], Literal[E.b]]) 74 | 75 | 76 | def test_tuple(): 77 | validate("(int, str)", Tuple[int, str]) 78 | 79 | 80 | def test_tuple_nested(): 81 | validate("List[(int, str),]", List[Tuple[int, str]]) 82 | 83 | 84 | def test_typeguard(): 85 | validate("x is 1", TypeIs[Literal[1]]) 86 | 87 | 88 | def test_typeguard_asymmetric(): 89 | validate("x is 1 if True else False", TypeGuard[Literal[1]]) 90 | 91 | 92 | def test_callable(): 93 | validate("(str) -> int", Callable[[str], int]) 94 | 95 | 96 | def test_function(): 97 | validate("def (str) -> int", Callable[[str], int]) 98 | validate("FunctionType[[str], int]", Callable[[str], int]) 99 | 100 | 101 | def_ = int 102 | 103 | 104 | def test_adversarial_function(): 105 | validate("Union[def_, '() -> int']", Union[def_, Callable[[], int]]) 106 | 107 | 108 | def test_functiontype(): 109 | validate("FunctionType[[str], int]", Callable[[str], int]) 110 | 111 | 112 | def test_intersection(): 113 | validate("int & str", Intersection[int, str]) 114 | 115 | 116 | def test_annotated(): 117 | validate("Annotated[1, 1]", Annotated[Literal[1], 1]) 118 | 119 | 120 | def test_syntax_error(): 121 | with raises(SyntaxError): 122 | validate("among us", None) 123 | 124 | 125 | def test_unsupported(): 126 | with raises(TypeError): 127 | validate("int + str", None) 128 | -------------------------------------------------------------------------------- /tests/test_typeform.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from basedtyping import TypeForm 4 | 5 | 6 | class A: 7 | x: int 8 | 9 | 10 | class B: 11 | y: int 12 | 13 | 14 | def test_typeform(): 15 | assert str(TypeForm[A]) == f"basedtyping.TypeForm[{A.__module__}.{A.__qualname__}]" 16 | -------------------------------------------------------------------------------- /tests/test_typetime_only/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinIsland/basedtyping/f3f3c7461441f4a737708b4d0b650c6677f55ee4/tests/test_typetime_only/__init__.py -------------------------------------------------------------------------------- /tests/test_typetime_only/test_assert_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | # these are just type-time tests, not real life pytest tests. they are only run by mypy 7 | from basedtyping.typetime_only import assert_type 8 | 9 | def test_assert_type_pass() -> None: 10 | assert_type[int](1) 11 | 12 | def test_assert_type_fail() -> None: 13 | assert_type[int]("") # type: ignore[arg-type] 14 | -------------------------------------------------------------------------------- /tests/test_typetime_only/test_typetime_only.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pytest import raises 4 | 5 | 6 | def test_runtime_import() -> None: 7 | with raises(ImportError): 8 | import basedtyping.typetime_only # noqa: F401 9 | --------------------------------------------------------------------------------