Compare commits
132 Commits
micha/erro
...
micha/symb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
312153e96d | ||
|
|
913bce3cd5 | ||
|
|
6195c026ff | ||
|
|
c0b0491703 | ||
|
|
51863b460b | ||
|
|
14a5a2629e | ||
|
|
3aed14935d | ||
|
|
bcec5e615b | ||
|
|
a06099dffe | ||
|
|
bb43085939 | ||
|
|
c1eaf6ff72 | ||
|
|
3bb0dac235 | ||
|
|
40cba5dc8a | ||
|
|
596d80cc8e | ||
|
|
d8b9a366c8 | ||
|
|
85e71ba91a | ||
|
|
2802cbde29 | ||
|
|
ed2bce6ebb | ||
|
|
f0012df686 | ||
|
|
0fc4e8f795 | ||
|
|
ac81c72bf3 | ||
|
|
c0b7c36d43 | ||
|
|
e8e461da6a | ||
|
|
91c9168dd7 | ||
|
|
80577a49f8 | ||
|
|
f463fa7b7c | ||
|
|
c3b6139f39 | ||
|
|
c9fdb1f5e3 | ||
|
|
463046ae07 | ||
|
|
dcb99cc817 | ||
|
|
7c2e7cf25e | ||
|
|
867a8f9497 | ||
|
|
e22718f25f | ||
|
|
dcdc6e7c64 | ||
|
|
4ddf9228f6 | ||
|
|
6d72be2683 | ||
|
|
712c886749 | ||
|
|
50739f91dc | ||
|
|
6a5eff6017 | ||
|
|
a623d8f7c4 | ||
|
|
aa429b413f | ||
|
|
425c248232 | ||
|
|
bcd944347d | ||
|
|
86eff81c6a | ||
|
|
24ace68560 | ||
|
|
b664505d7b | ||
|
|
aa575da1e7 | ||
|
|
921eb2acb3 | ||
|
|
8665d2dc95 | ||
|
|
1cc27c995c | ||
|
|
a93bc2af6b | ||
|
|
d848182340 | ||
|
|
7173e6a20b | ||
|
|
4a7536dc94 | ||
|
|
2d15d7d1af | ||
|
|
112e9d2d82 | ||
|
|
1389cb8e59 | ||
|
|
fa46ba2306 | ||
|
|
53c7ef8bfe | ||
|
|
4d64cdb83c | ||
|
|
ac31b26a0e | ||
|
|
a80e934838 | ||
|
|
224c8438bd | ||
|
|
90a5439791 | ||
|
|
4b2b126b9f | ||
|
|
9798556eb5 | ||
|
|
aa1938f6ba | ||
|
|
3533d7f5b4 | ||
|
|
0bbe166720 | ||
|
|
c3a64b44b7 | ||
|
|
dfd7f38009 | ||
|
|
e96b13c027 | ||
|
|
f52b1f4a4d | ||
|
|
1c8f356e07 | ||
|
|
2ccc9b19a7 | ||
|
|
c1837e4189 | ||
|
|
d7ce548893 | ||
|
|
be4ce16735 | ||
|
|
657d26ff20 | ||
|
|
dbc191d2d6 | ||
|
|
d2712c7669 | ||
|
|
e5cb4d6388 | ||
|
|
68e8496260 | ||
|
|
71239f248e | ||
|
|
58930905eb | ||
|
|
53f2d72e02 | ||
|
|
3629cbf35a | ||
|
|
37f433814c | ||
|
|
45b565cbb5 | ||
|
|
82faa9bb62 | ||
|
|
2eac00c60f | ||
|
|
033ecf5a4b | ||
|
|
5509a3d7ae | ||
|
|
e4885a2fb2 | ||
|
|
a7e5e42b88 | ||
|
|
c361cf66ad | ||
|
|
a54353392f | ||
|
|
ef153a0cce | ||
|
|
0c85023cd9 | ||
|
|
7135a49aea | ||
|
|
6e11086c98 | ||
|
|
28653c7c47 | ||
|
|
1d91dae11f | ||
|
|
01d16e8941 | ||
|
|
881375a8d9 | ||
|
|
6f8d8fa36b | ||
|
|
f30227c436 | ||
|
|
c8d505c8ea | ||
|
|
a55722e740 | ||
|
|
a3bb0cd5ec | ||
|
|
d4126f6049 | ||
|
|
03fb2e5ac1 | ||
|
|
1a3c311ac5 | ||
|
|
2ecd164adb | ||
|
|
5fc8e5d80e | ||
|
|
5f548072d9 | ||
|
|
dc0d944608 | ||
|
|
15fe540251 | ||
|
|
7a0e9b34d0 | ||
|
|
4b8c815b27 | ||
|
|
e3f34b8f5b | ||
|
|
ab26d9cf9a | ||
|
|
64944f2cf5 | ||
|
|
533e8a6ee6 | ||
|
|
c62ba48ad4 | ||
|
|
68eb0a2511 | ||
|
|
0f4350e10e | ||
|
|
aa6b812a73 | ||
|
|
0e9427255f | ||
|
|
9c3c59aca9 | ||
|
|
172143ae77 | ||
|
|
3865fb6641 |
9
.github/actionlint.yaml
vendored
Normal file
9
.github/actionlint.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Configuration for the actionlint tool, which we run via pre-commit
|
||||
# to verify the correctness of the syntax in our GitHub Actions workflows.
|
||||
|
||||
self-hosted-runner:
|
||||
# Various runners we use that aren't recognized out-of-the-box by actionlint:
|
||||
labels:
|
||||
- depot-ubuntu-latest-8
|
||||
- depot-ubuntu-22.04-16
|
||||
- windows-latest-xlarge
|
||||
20
.github/workflows/build-binaries.yml
vendored
20
.github/workflows/build-binaries.yml
vendored
@@ -53,9 +53,9 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test sdist"
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
|
||||
${{ env.MODULE_NAME }} --help
|
||||
python -m ${{ env.MODULE_NAME }} --help
|
||||
pip install dist/"${PACKAGE_NAME}"-*.tar.gz --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload sdist"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - aarch64"
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
@@ -186,9 +186,9 @@ jobs:
|
||||
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
${{ env.MODULE_NAME }} --help
|
||||
python -m ${{ env.MODULE_NAME }} --help
|
||||
python -m pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -236,9 +236,9 @@ jobs:
|
||||
- name: "Test wheel"
|
||||
if: ${{ startsWith(matrix.target, 'x86_64') }}
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
${{ env.MODULE_NAME }} --help
|
||||
python -m ${{ env.MODULE_NAME }} --help
|
||||
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
18
.github/workflows/build-docker.yml
vendored
18
.github/workflows/build-docker.yml
vendored
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Normalize Platform Pair (replace / with -)
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV
|
||||
echo "PLATFORM_TUPLE=${platform//\//-}" >> "$GITHUB_ENV"
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Build and push by digest
|
||||
@@ -87,9 +87,10 @@ jobs:
|
||||
outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
|
||||
- name: Export digests
|
||||
env:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digests
|
||||
@@ -141,9 +142,10 @@ jobs:
|
||||
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
|
||||
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
|
||||
run: |
|
||||
# shellcheck disable=SC2046
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
|
||||
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
|
||||
docker-publish-extra:
|
||||
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
|
||||
@@ -182,7 +184,7 @@ jobs:
|
||||
# Generate Dockerfile content
|
||||
cat <<EOF > Dockerfile
|
||||
FROM ${BASE_IMAGE}
|
||||
COPY --from=${{ env.RUFF_BASE_IMG }}:latest /ruff /usr/local/bin/ruff
|
||||
COPY --from=${RUFF_BASE_IMG}:latest /ruff /usr/local/bin/ruff
|
||||
ENTRYPOINT []
|
||||
CMD ["/usr/local/bin/ruff"]
|
||||
EOF
|
||||
@@ -202,14 +204,14 @@ jobs:
|
||||
TAG_PATTERNS="${TAG_PATTERNS%\\n}"
|
||||
|
||||
# Export image cache name
|
||||
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV
|
||||
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> "$GITHUB_ENV"
|
||||
|
||||
# Export tag patterns using the multiline env var syntax
|
||||
{
|
||||
echo "TAG_PATTERNS<<EOF"
|
||||
echo -e "${TAG_PATTERNS}"
|
||||
echo EOF
|
||||
} >> $GITHUB_ENV
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
@@ -285,7 +287,9 @@ jobs:
|
||||
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
|
||||
run: |
|
||||
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
|
||||
|
||||
# shellcheck disable=SC2046
|
||||
docker buildx imagetools create \
|
||||
"${annotations[@]}" \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
|
||||
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
|
||||
72
.github/workflows/ci.yaml
vendored
72
.github/workflows/ci.yaml
vendored
@@ -290,7 +290,9 @@ jobs:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup default ${{ steps.msrv.outputs.value }}
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: rustup default "${MSRV}"
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
@@ -306,13 +308,14 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-fuzz-build:
|
||||
name: "cargo fuzz build"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -354,16 +357,18 @@ jobs:
|
||||
name: ruff
|
||||
path: ruff-to-test
|
||||
- name: Fuzz
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.download-cached-binary.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
chmod +x "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
(
|
||||
uvx \
|
||||
--python=${{ env.PYTHON_VERSION }} \
|
||||
--python="${PYTHON_VERSION}" \
|
||||
--from=./python/py-fuzzer \
|
||||
fuzz \
|
||||
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
|
||||
--test-executable="${DOWNLOAD_PATH}/ruff" \
|
||||
--bin=ruff \
|
||||
0-500
|
||||
)
|
||||
@@ -429,64 +434,72 @@ jobs:
|
||||
|
||||
- name: Run `ruff check` stable ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
|
||||
cat ecosystem-result-check-stable > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-check-stable > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Linter (stable)" > ecosystem-result
|
||||
cat ecosystem-result-check-stable >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff check` preview ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
|
||||
cat ecosystem-result-check-preview > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-check-preview > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Linter (preview)" >> ecosystem-result
|
||||
cat ecosystem-result-check-preview >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff format` stable ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
|
||||
cat ecosystem-result-format-stable > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-format-stable > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Formatter (stable)" >> ecosystem-result
|
||||
cat ecosystem-result-format-stable >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff format` preview ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
|
||||
cat ecosystem-result-format-preview > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-format-preview > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Formatter (preview)" >> ecosystem-result
|
||||
cat ecosystem-result-format-preview >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
@@ -541,7 +554,7 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
run: |
|
||||
pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }}
|
||||
pip install --force-reinstall --find-links dist "${PACKAGE_NAME}"
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Remove wheels from cache"
|
||||
@@ -570,13 +583,14 @@ jobs:
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: "Run pre-commit"
|
||||
run: |
|
||||
echo '```console' > $GITHUB_STEP_SUMMARY
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> $GITHUB_STEP_SUMMARY) >&1
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit $exit_code
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
exit "$exit_code"
|
||||
|
||||
docs:
|
||||
name: "mkdocs"
|
||||
@@ -637,7 +651,7 @@ jobs:
|
||||
- name: "Run checks"
|
||||
run: scripts/formatter_ecosystem_checks.sh
|
||||
- name: "Github step summary"
|
||||
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
|
||||
run: cat target/formatter-ecosystem/stats.txt > "$GITHUB_STEP_SUMMARY"
|
||||
- name: "Remove checkouts from cache"
|
||||
run: rm -r target/formatter-ecosystem
|
||||
|
||||
@@ -676,11 +690,13 @@ jobs:
|
||||
just install
|
||||
|
||||
- name: Run ruff-lsp tests
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Setup development binary
|
||||
pip uninstall --yes ruff
|
||||
chmod +x ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
export PATH=${{ steps.ruff-target.outputs.download-path }}:$PATH
|
||||
chmod +x "${DOWNLOAD_PATH}/ruff"
|
||||
export PATH="${DOWNLOAD_PATH}:${PATH}"
|
||||
ruff version
|
||||
|
||||
just test
|
||||
|
||||
1
.github/workflows/daily_fuzz.yaml
vendored
1
.github/workflows/daily_fuzz.yaml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
run: cargo build --locked
|
||||
- name: Fuzz
|
||||
run: |
|
||||
# shellcheck disable=SC2046
|
||||
(
|
||||
uvx \
|
||||
--python=3.12 \
|
||||
|
||||
13
.github/workflows/pr-comment.yaml
vendored
13
.github/workflows/pr-comment.yaml
vendored
@@ -10,12 +10,11 @@ on:
|
||||
description: The ecosystem workflow that triggers the workflow run
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
name: Download pull request number
|
||||
@@ -30,7 +29,7 @@ jobs:
|
||||
run: |
|
||||
if [[ -f pr-number ]]
|
||||
then
|
||||
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
@@ -66,9 +65,9 @@ jobs:
|
||||
cat pr/ecosystem/ecosystem-result >> comment.txt
|
||||
echo "" >> comment.txt
|
||||
|
||||
echo 'comment<<EOF' >> $GITHUB_OUTPUT
|
||||
cat comment.txt >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
|
||||
cat comment.txt >> "$GITHUB_OUTPUT"
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
|
||||
42
.github/workflows/publish-docs.yml
vendored
42
.github/workflows/publish-docs.yml
vendored
@@ -44,21 +44,19 @@ jobs:
|
||||
# Use version as display name for now
|
||||
display_name="$version"
|
||||
|
||||
echo "version=$version" >> $GITHUB_ENV
|
||||
echo "display_name=$display_name" >> $GITHUB_ENV
|
||||
echo "version=$version" >> "$GITHUB_ENV"
|
||||
echo "display_name=$display_name" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Set branch name"
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
display_name="${{ env.display_name }}"
|
||||
timestamp="$(date +%s)"
|
||||
|
||||
# create branch_display_name from display_name by replacing all
|
||||
# characters disallowed in git branch names with hyphens
|
||||
branch_display_name="$(echo "$display_name" | tr -c '[:alnum:]._' '-' | tr -s '-')"
|
||||
branch_display_name="$(echo "${display_name}" | tr -c '[:alnum:]._' '-' | tr -s '-')"
|
||||
|
||||
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV
|
||||
echo "timestamp=$timestamp" >> $GITHUB_ENV
|
||||
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
|
||||
echo "timestamp=$timestamp" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
@@ -93,9 +91,7 @@ jobs:
|
||||
run: mkdocs build --strict -f mkdocs.public.yml
|
||||
|
||||
- name: "Clone docs repo"
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
|
||||
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
|
||||
|
||||
- name: "Copy docs"
|
||||
run: rm -rf astral-docs/site/ruff && mkdir -p astral-docs/site && cp -r site/ruff astral-docs/site/
|
||||
@@ -103,12 +99,10 @@ jobs:
|
||||
- name: "Commit docs"
|
||||
working-directory: astral-docs
|
||||
run: |
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
git config user.name "astral-docs-bot"
|
||||
git config user.email "176161322+astral-docs-bot@users.noreply.github.com"
|
||||
|
||||
git checkout -b $branch_name
|
||||
git checkout -b "${branch_name}"
|
||||
git add site/ruff
|
||||
git commit -m "Update ruff documentation for $version"
|
||||
|
||||
@@ -117,12 +111,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
display_name="${{ env.display_name }}"
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
# set the PR title
|
||||
pull_request_title="Update ruff documentation for $display_name"
|
||||
pull_request_title="Update ruff documentation for ${display_name}"
|
||||
|
||||
# Delete any existing pull requests that are open for this version
|
||||
# by checking against pull_request_title because the new PR will
|
||||
@@ -131,13 +121,15 @@ jobs:
|
||||
xargs -I {} gh pr close {}
|
||||
|
||||
# push the branch to GitHub
|
||||
git push origin $branch_name
|
||||
git push origin "${branch_name}"
|
||||
|
||||
# create the PR
|
||||
gh pr create --base main --head $branch_name \
|
||||
--title "$pull_request_title" \
|
||||
--body "Automated documentation update for $display_name" \
|
||||
--label "documentation"
|
||||
gh pr create \
|
||||
--base=main \
|
||||
--head="${branch_name}" \
|
||||
--title="${pull_request_title}" \
|
||||
--body="Automated documentation update for ${display_name}" \
|
||||
--label="documentation"
|
||||
|
||||
- name: "Merge Pull Request"
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
@@ -145,9 +137,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
# auto-merge the PR if the build was triggered by a release. Manual builds should be reviewed by a human.
|
||||
# give the PR a few seconds to be created before trying to auto-merge it
|
||||
sleep 10
|
||||
gh pr merge --squash $branch_name
|
||||
gh pr merge --squash "${branch_name}"
|
||||
|
||||
4
.github/workflows/sync_typeshed.yaml
vendored
4
.github/workflows/sync_typeshed.yaml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
repository: python/typeshed
|
||||
path: typeshed
|
||||
persist-credentials: true
|
||||
persist-credentials: false
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
run: |
|
||||
cd ruff
|
||||
git push --force origin typeshedbot/sync-typeshed
|
||||
gh pr list --repo $GITHUB_REPOSITORY --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
|
||||
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
|
||||
|
||||
create-issue-on-failure:
|
||||
|
||||
6
.github/zizmor.yml
vendored
Normal file
6
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://woodruffw.github.io/zizmor/configuration/
|
||||
rules:
|
||||
dangerous-triggers:
|
||||
ignore:
|
||||
- pr-comment.yaml
|
||||
@@ -21,3 +21,11 @@ MD014: false
|
||||
MD024:
|
||||
# Allow when nested under different parents e.g. CHANGELOG.md
|
||||
siblings_only: true
|
||||
|
||||
# MD046/code-block-style
|
||||
#
|
||||
# Ignore this because it conflicts with the code block style used in content
|
||||
# tabs of mkdocs-material which is to add a blank line after the content title.
|
||||
#
|
||||
# Ref: https://github.com/astral-sh/ruff/pull/15011#issuecomment-2544790854
|
||||
MD046: false
|
||||
|
||||
@@ -2,6 +2,7 @@ fail_fast: false
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.github/workflows/release.yml|
|
||||
crates/red_knot_vendored/vendor/.*|
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
@@ -26,9 +27,8 @@ repos:
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
- mdformat-footnote
|
||||
- mdformat-mkdocs==4.0.0
|
||||
- mdformat-footnote==0.1.1
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/formatter/black\.md
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.2
|
||||
rev: v1.28.3
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.2
|
||||
rev: v0.8.3
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -88,20 +88,37 @@ repos:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v0.8.0
|
||||
rev: v0.9.2
|
||||
hooks:
|
||||
- id: zizmor
|
||||
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
|
||||
# (https://opensource.axo.dev/cargo-dist/)
|
||||
exclude: .github/workflows/release.yml
|
||||
# We could consider enabling the low-severity warnings, but they're noisy
|
||||
args: [--min-severity=medium]
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.30.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
|
||||
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.4
|
||||
hooks:
|
||||
- id: actionlint
|
||||
stages:
|
||||
# This hook is disabled by default, since it's quite slow.
|
||||
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
|
||||
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
|
||||
- manual
|
||||
args:
|
||||
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
|
||||
- "-ignore=SC2016" # another shellcheck lint: seems to have false positives?
|
||||
additional_dependencies:
|
||||
# actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions
|
||||
# and checks these with shellcheck. This is arguably its most useful feature,
|
||||
# but the integration only works if shellcheck is installed
|
||||
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0"
|
||||
|
||||
ci:
|
||||
skip: [cargo-fmt, dev-generate-all]
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,5 +1,68 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend `AIR302` with additional functions and classes ([#15015](https://github.com/astral-sh/ruff/pull/15015))
|
||||
- \[`airflow`\] Implement `moved-to-provider-in-3` for modules that has been moved to Airflow providers (`AIR303`) ([#14764](https://github.com/astral-sh/ruff/pull/14764))
|
||||
- \[`flake8-use-pathlib`\] Extend check for invalid path suffix to include the case `"."` (`PTH210`) ([#14902](https://github.com/astral-sh/ruff/pull/14902))
|
||||
- \[`perflint`\] Fix panic in `PERF401` when list variable is after the `for` loop ([#14971](https://github.com/astral-sh/ruff/pull/14971))
|
||||
- \[`perflint`\] Simplify finding the loop target in `PERF401` ([#15025](https://github.com/astral-sh/ruff/pull/15025))
|
||||
- \[`pylint`\] Preserve original value format (`PLR6104`) ([#14978](https://github.com/astral-sh/ruff/pull/14978))
|
||||
- \[`ruff`\] Avoid false positives for `RUF027` for typing context bindings ([#15037](https://github.com/astral-sh/ruff/pull/15037))
|
||||
- \[`ruff`\] Check for ambiguous pattern passed to `pytest.raises()` (`RUF043`) ([#14966](https://github.com/astral-sh/ruff/pull/14966))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bandit`\] Check `S105` for annotated assignment ([#15059](https://github.com/astral-sh/ruff/pull/15059))
|
||||
- \[`flake8-pyi`\] More autofixes for `redundant-none-literal` (`PYI061`) ([#14872](https://github.com/astral-sh/ruff/pull/14872))
|
||||
- \[`pydocstyle`\] Skip leading whitespace for `D403` ([#14963](https://github.com/astral-sh/ruff/pull/14963))
|
||||
- \[`ruff`\] Skip `SQLModel` base classes for `mutable-class-default` (`RUF012`) ([#14949](https://github.com/astral-sh/ruff/pull/14949))
|
||||
|
||||
### Bug
|
||||
|
||||
- \[`perflint`\] Parenthesize walrus expressions in autofix for `manual-list-comprehension` (`PERF401`) ([#15050](https://github.com/astral-sh/ruff/pull/15050))
|
||||
|
||||
### Server
|
||||
|
||||
- Check diagnostic refresh support from client capability which enables dynamic configuration for various editors ([#15014](https://github.com/astral-sh/ruff/pull/15014))
|
||||
|
||||
## 0.8.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix fstring formatting removing overlong implicit concatenated string in expression part ([#14811](https://github.com/astral-sh/ruff/pull/14811))
|
||||
- \[`airflow`\] Add fix to remove deprecated keyword arguments (`AIR302`) ([#14887](https://github.com/astral-sh/ruff/pull/14887))
|
||||
- \[`airflow`\]: Extend rule to include deprecated names for Airflow 3.0 (`AIR302`) ([#14765](https://github.com/astral-sh/ruff/pull/14765) and [#14804](https://github.com/astral-sh/ruff/pull/14804))
|
||||
- \[`flake8-bugbear`\] Improve error messages for `except*` (`B025`, `B029`, `B030`, `B904`) ([#14815](https://github.com/astral-sh/ruff/pull/14815))
|
||||
- \[`flake8-bugbear`\] `itertools.batched()` without explicit `strict` (`B911`) ([#14408](https://github.com/astral-sh/ruff/pull/14408))
|
||||
- \[`flake8-use-pathlib`\] Dotless suffix passed to `Path.with_suffix()` (`PTH210`) ([#14779](https://github.com/astral-sh/ruff/pull/14779))
|
||||
- \[`pylint`\] Include parentheses and multiple comparators in check for `boolean-chained-comparison` (`PLR1716`) ([#14781](https://github.com/astral-sh/ruff/pull/14781))
|
||||
- \[`ruff`\] Do not simplify `round()` calls (`RUF046`) ([#14832](https://github.com/astral-sh/ruff/pull/14832))
|
||||
- \[`ruff`\] Don't emit `used-dummy-variable` on function parameters (`RUF052`) ([#14818](https://github.com/astral-sh/ruff/pull/14818))
|
||||
- \[`ruff`\] Implement `if-key-in-dict-del` (`RUF051`) ([#14553](https://github.com/astral-sh/ruff/pull/14553))
|
||||
- \[`ruff`\] Mark autofix for `RUF052` as always unsafe ([#14824](https://github.com/astral-sh/ruff/pull/14824))
|
||||
- \[`ruff`\] Teach autofix for `used-dummy-variable` about TypeVars etc. (`RUF052`) ([#14819](https://github.com/astral-sh/ruff/pull/14819))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Offer unsafe autofix for `no-explicit-stacklevel` (`B028`) ([#14829](https://github.com/astral-sh/ruff/pull/14829))
|
||||
- \[`flake8-pyi`\] Skip all type definitions in `string-or-bytes-too-long` (`PYI053`) ([#14797](https://github.com/astral-sh/ruff/pull/14797))
|
||||
- \[`pyupgrade`\] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (`UP009`) ([#14728](https://github.com/astral-sh/ruff/pull/14728))
|
||||
- \[`pyupgrade`\] Mark fixes for `convert-typed-dict-functional-to-class` and `convert-named-tuple-functional-to-class` as unsafe if they will remove comments (`UP013`, `UP014`) ([#14842](https://github.com/astral-sh/ruff/pull/14842))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Raise syntax error for mixing `except` and `except*` ([#14895](https://github.com/astral-sh/ruff/pull/14895))
|
||||
- \[`flake8-bugbear`\] Fix `B028` to allow `stacklevel` to be explicitly assigned as a positional argument ([#14868](https://github.com/astral-sh/ruff/pull/14868))
|
||||
- \[`flake8-bugbear`\] Skip `B028` if `warnings.warn` is called with `*args` or `**kwargs` ([#14870](https://github.com/astral-sh/ruff/pull/14870))
|
||||
- \[`flake8-comprehensions`\] Skip iterables with named expressions in `unnecessary-map` (`C417`) ([#14827](https://github.com/astral-sh/ruff/pull/14827))
|
||||
- \[`flake8-pyi`\] Also remove `self` and `cls`'s annotation (`PYI034`) ([#14801](https://github.com/astral-sh/ruff/pull/14801))
|
||||
- \[`flake8-pytest-style`\] Fix `pytest-parametrize-names-wrong-type` (`PT006`) to edit both `argnames` and `argvalues` if both of them are single-element tuples/lists ([#14699](https://github.com/astral-sh/ruff/pull/14699))
|
||||
- \[`perflint`\] Improve autofix for `PERF401` ([#14369](https://github.com/astral-sh/ruff/pull/14369))
|
||||
- \[`pylint`\] Fix `PLW1508` false positive for default string created via a mult operation ([#14841](https://github.com/astral-sh/ruff/pull/14841))
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Preview features
|
||||
|
||||
73
Cargo.lock
generated
73
Cargo.lock
generated
@@ -220,9 +220,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
|
||||
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
@@ -314,9 +314,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -465,12 +465,12 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -923,9 +923,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
|
||||
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
@@ -1521,9 +1521,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.167"
|
||||
version = "0.2.168"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
|
||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -2161,7 +2161,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2288,6 +2288,7 @@ dependencies = [
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
"drop_bomb",
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"insta",
|
||||
@@ -2300,6 +2301,7 @@ dependencies = [
|
||||
"red_knot_vendored",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_literal",
|
||||
"ruff_python_parser",
|
||||
@@ -2313,7 +2315,7 @@ dependencies = [
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2410,7 +2412,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2516,7 +2518,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2563,7 +2565,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -2633,7 +2635,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -2735,7 +2737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2785,7 +2787,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -2819,7 +2821,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2891,7 +2893,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3024,7 +3026,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.6",
|
||||
"thiserror 2.0.7",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -3050,7 +3052,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3192,7 +3194,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -3202,6 +3204,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
@@ -3212,12 +3215,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3279,9 +3282,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.215"
|
||||
version = "1.0.216"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3299,9 +3302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.215"
|
||||
version = "1.0.216"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3622,11 +3625,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
|
||||
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.6",
|
||||
"thiserror-impl 2.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3642,9 +3645,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
|
||||
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -118,7 +118,8 @@ rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -140,8 +140,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.4/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -174,7 +174,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.2
|
||||
rev: v0.8.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use python_version::PythonVersion;
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
@@ -15,12 +16,11 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use target_version::TargetVersion;
|
||||
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
|
||||
mod logging;
|
||||
mod target_version;
|
||||
mod python_version;
|
||||
mod verbosity;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -34,54 +34,39 @@ struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Option<Command>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Changes the current working directory.",
|
||||
long_help = "Changes the current working directory before any specified operations. This affects the workspace and configuration discovery.",
|
||||
value_name = "PATH"
|
||||
)]
|
||||
current_directory: Option<SystemPathBuf>,
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
/// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set.
|
||||
///
|
||||
/// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
|
||||
#[arg(long, value_name = "PROJECT")]
|
||||
project: Option<SystemPathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Path to the virtual environment the project uses",
|
||||
long_help = "\
|
||||
Path to the virtual environment the project uses. \
|
||||
If provided, red-knot will use the `site-packages` directory of this virtual environment \
|
||||
to resolve type information for the project's third-party dependencies.",
|
||||
value_name = "PATH"
|
||||
)]
|
||||
/// Path to the virtual environment the project uses.
|
||||
///
|
||||
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
|
||||
/// to resolve type information for the project's third-party dependencies.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
venv_path: Option<SystemPathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIRECTORY",
|
||||
help = "Custom directory to use for stdlib typeshed stubs"
|
||||
)]
|
||||
custom_typeshed_dir: Option<SystemPathBuf>,
|
||||
/// Custom directory to use for stdlib typeshed stubs.
|
||||
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
|
||||
typeshed: Option<SystemPathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PATH",
|
||||
help = "Additional path to use as a module-resolution source (can be passed multiple times)"
|
||||
)]
|
||||
/// Additional path to use as a module-resolution source (can be passed multiple times).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Python version to assume when resolving types",
|
||||
value_name = "VERSION"
|
||||
)]
|
||||
target_version: Option<TargetVersion>,
|
||||
/// Python version to assume when resolving types.
|
||||
#[arg(long, value_name = "VERSION", alias = "target-version")]
|
||||
python_version: Option<PythonVersion>,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Run in watch mode by re-running whenever files change",
|
||||
short = 'W'
|
||||
)]
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(long, short = 'W')]
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
@@ -89,8 +74,8 @@ impl Args {
|
||||
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
|
||||
let mut configuration = Configuration::default();
|
||||
|
||||
if let Some(target_version) = self.target_version {
|
||||
configuration.target_version = Some(target_version.into());
|
||||
if let Some(python_version) = self.python_version {
|
||||
configuration.python_version = Some(python_version.into());
|
||||
}
|
||||
|
||||
if let Some(venv_path) = &self.venv_path {
|
||||
@@ -99,9 +84,8 @@ impl Args {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(custom_typeshed_dir) = &self.custom_typeshed_dir {
|
||||
configuration.search_paths.custom_typeshed =
|
||||
Some(SystemPath::absolute(custom_typeshed_dir, cli_cwd));
|
||||
if let Some(typeshed) = &self.typeshed {
|
||||
configuration.search_paths.typeshed = Some(SystemPath::absolute(typeshed, cli_cwd));
|
||||
}
|
||||
|
||||
if let Some(extra_search_paths) = &self.extra_search_path {
|
||||
@@ -167,15 +151,13 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
};
|
||||
|
||||
let cwd = args
|
||||
.current_directory
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|cwd| {
|
||||
if cwd.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(cwd, &cli_base_path))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided current-directory path `{cwd}` is not a directory"
|
||||
))
|
||||
Err(anyhow!("Provided project path `{cwd}` is not a directory"))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
@@ -297,7 +279,7 @@ impl MainLoop {
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.snapshot();
|
||||
let db = db.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the workspace. This needs to be done in a separate thread
|
||||
|
||||
68
crates/red_knot/src/python_version.rs
Normal file
68
crates/red_knot/src/python_version.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum PythonVersion {
|
||||
#[value(name = "3.7")]
|
||||
Py37,
|
||||
#[value(name = "3.8")]
|
||||
Py38,
|
||||
#[default]
|
||||
#[value(name = "3.9")]
|
||||
Py39,
|
||||
#[value(name = "3.10")]
|
||||
Py310,
|
||||
#[value(name = "3.11")]
|
||||
Py311,
|
||||
#[value(name = "3.12")]
|
||||
Py312,
|
||||
#[value(name = "3.13")]
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "3.7",
|
||||
Self::Py38 => "3.8",
|
||||
Self::Py39 => "3.9",
|
||||
Self::Py310 => "3.10",
|
||||
Self::Py311 => "3.11",
|
||||
Self::Py312 => "3.12",
|
||||
Self::Py313 => "3.13",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PythonVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
match value {
|
||||
PythonVersion::Py37 => Self::PY37,
|
||||
PythonVersion::Py38 => Self::PY38,
|
||||
PythonVersion::Py39 => Self::PY39,
|
||||
PythonVersion::Py310 => Self::PY310,
|
||||
PythonVersion::Py311 => Self::PY311,
|
||||
PythonVersion::Py312 => Self::PY312,
|
||||
PythonVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
|
||||
red_knot_python_semantic::PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
Py38,
|
||||
#[default]
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl TargetVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::PY37,
|
||||
TargetVersion::Py38 => Self::PY38,
|
||||
TargetVersion::Py39 => Self::PY39,
|
||||
TargetVersion::Py310 => Self::PY310,
|
||||
TargetVersion::Py311 => Self::PY311,
|
||||
TargetVersion::Py312 => Self::PY312,
|
||||
TargetVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::target_version::TargetVersion;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
PythonVersion::from(TargetVersion::default()),
|
||||
PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -282,7 +282,7 @@ where
|
||||
.extra_paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(search_paths.custom_typeshed.iter())
|
||||
.chain(search_paths.typeshed.iter())
|
||||
.chain(search_paths.site_packages.iter().flat_map(|site_packages| {
|
||||
if let SitePackages::Known(path) = site_packages {
|
||||
path.as_slice()
|
||||
@@ -296,7 +296,7 @@ where
|
||||
}
|
||||
|
||||
let configuration = Configuration {
|
||||
target_version: Some(PythonVersion::PY312),
|
||||
python_version: Some(PythonVersion::PY312),
|
||||
search_paths,
|
||||
};
|
||||
|
||||
@@ -888,7 +888,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
},
|
||||
|root_path, _workspace_path| SearchPathConfiguration {
|
||||
custom_typeshed: Some(root_path.join("typeshed")),
|
||||
typeshed: Some(root_path.join("typeshed")),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
@@ -25,6 +26,7 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
drop_bomb = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
@@ -52,5 +54,8 @@ tempfile = { workspace = true }
|
||||
quickcheck = { version = "1.0.3", default-features = false }
|
||||
quickcheck_macros = { version = "1.0.0" }
|
||||
|
||||
[features]
|
||||
serde = ["ruff_db/serde", "dep:serde"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# `Annotated`
|
||||
|
||||
`Annotated` attaches arbitrary metadata to a given type.
|
||||
|
||||
## Usages
|
||||
|
||||
`Annotated[T, ...]` is equivalent to `T`: All metadata arguments are simply ignored.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
def _(x: Annotated[int, "foo"]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[int, lambda: 0 + 1 * 2 // 3, _(4)]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[int, "arbitrary", "metadata", "elements", "are", "fine"]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[tuple[str, int], bytes]):
|
||||
reveal_type(x) # revealed: tuple[str, int]
|
||||
```
|
||||
|
||||
## Parameterization
|
||||
|
||||
It is invalid to parameterize `Annotated` with less than two arguments.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
def _(x: Annotated):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
X = Annotated
|
||||
else:
|
||||
X = bool
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
def _(x: Annotated | bool):
|
||||
reveal_type(x) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[()]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[int]):
|
||||
# `Annotated[T]` is invalid and will raise an error at runtime,
|
||||
# but we treat it the same as `T` to provide better diagnostics later on.
|
||||
# The subscription itself is still reported, regardless.
|
||||
# Same for the `(int,)` form below.
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[(int,)]):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
### Correctly parameterized
|
||||
|
||||
Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# TODO: False positive
|
||||
# error: [invalid-base]
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# At runtime, this is an error.
|
||||
# error: [invalid-base]
|
||||
class C(Annotated): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
```
|
||||
@@ -34,8 +34,7 @@ If you define your own class named `Any`, using that in a type expression refers
|
||||
isn't a spelling of the Any type.
|
||||
|
||||
```py
|
||||
class Any:
|
||||
pass
|
||||
class Any: ...
|
||||
|
||||
x: Any
|
||||
|
||||
@@ -59,8 +58,7 @@ assignable to `int`.
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Subclass(Any):
|
||||
pass
|
||||
class Subclass(Any): ...
|
||||
|
||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||
|
||||
@@ -68,8 +66,18 @@ x: Subclass = 1 # error: [invalid-assignment]
|
||||
# TODO: no diagnostic
|
||||
y: int = Subclass() # error: [invalid-assignment]
|
||||
|
||||
def f() -> Subclass:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Subclass
|
||||
def _(s: Subclass):
|
||||
reveal_type(s) # revealed: Subclass
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
`Any` cannot be parameterized:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
|
||||
def f(x: Any[int]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -45,19 +45,19 @@ def f():
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
# error: [invalid-literal-parameter]
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-literal-parameter]
|
||||
# error: [invalid-type-form]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-literal-parameter]
|
||||
# error: [invalid-type-form]
|
||||
invalid3: Literal[(3, 4)]
|
||||
|
||||
hello = "hello"
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-literal-parameter]
|
||||
1 + 2, # error: [invalid-type-form]
|
||||
"foo",
|
||||
hello, # error: [invalid-literal-parameter]
|
||||
(1, 2, 3), # error: [invalid-literal-parameter]
|
||||
hello, # error: [invalid-type-form]
|
||||
(1, 2, 3), # error: [invalid-type-form]
|
||||
]
|
||||
```
|
||||
|
||||
@@ -91,3 +91,13 @@ a1: Literal[26]
|
||||
def f():
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
# error: [invalid-type-form] "`Literal` requires at least one argument when used in a type expression"
|
||||
def _(x: Literal):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
@@ -27,19 +27,19 @@ def f():
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-type-form]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
|
||||
```
|
||||
|
||||
### Parametrized
|
||||
### Parameterized
|
||||
|
||||
`LiteralString` cannot be parametrized.
|
||||
`LiteralString` cannot be parameterized.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
a: LiteralString[str] # error: [invalid-type-parameter]
|
||||
b: LiteralString["foo"] # error: [invalid-type-parameter]
|
||||
a: LiteralString[str] # error: [invalid-type-form]
|
||||
b: LiteralString["foo"] # error: [invalid-type-form]
|
||||
```
|
||||
|
||||
### As a base class
|
||||
@@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(call todo)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(call todo)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
@@ -89,28 +89,26 @@ vice versa.
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
foo_1: Literal["foo"] = "foo"
|
||||
bar_1: LiteralString = foo_1 # fine
|
||||
|
||||
foo_1: Literal["foo"] = "foo"
|
||||
bar_1: LiteralString = foo_1 # fine
|
||||
foo_2 = "foo" if flag else "bar"
|
||||
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
|
||||
bar_2: LiteralString = foo_2 # fine
|
||||
|
||||
foo_2 = "foo" if coinflip() else "bar"
|
||||
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
|
||||
bar_2: LiteralString = foo_2 # fine
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
baz_1: str = str()
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
|
||||
baz_1: str = str()
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if coinflip() else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
baz_3 = "foo" if flag else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Narrowing
|
||||
@@ -137,7 +135,7 @@ if "" < lorem == "ipsum":
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -21,7 +21,7 @@ reveal_type(stop())
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
# error: [invalid-type-form] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
a2: Never
|
||||
@@ -51,7 +51,7 @@ def f():
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# Typing-module aliases to other stdlib classes
|
||||
|
||||
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
|
||||
still need to be supported by a type checker.
|
||||
|
||||
## Correspondence
|
||||
|
||||
All of the following symbols can be mapped one-to-one with the actual type:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
def f(
|
||||
list_bare: typing.List,
|
||||
list_parametrized: typing.List[int],
|
||||
dict_bare: typing.Dict,
|
||||
dict_parametrized: typing.Dict[int, str],
|
||||
set_bare: typing.Set,
|
||||
set_parametrized: typing.Set[int],
|
||||
frozen_set_bare: typing.FrozenSet,
|
||||
frozen_set_parametrized: typing.FrozenSet[str],
|
||||
chain_map_bare: typing.ChainMap,
|
||||
chain_map_parametrized: typing.ChainMap[int],
|
||||
counter_bare: typing.Counter,
|
||||
counter_parametrized: typing.Counter[int],
|
||||
default_dict_bare: typing.DefaultDict,
|
||||
default_dict_parametrized: typing.DefaultDict[str, int],
|
||||
deque_bare: typing.Deque,
|
||||
deque_parametrized: typing.Deque[str],
|
||||
ordered_dict_bare: typing.OrderedDict,
|
||||
ordered_dict_parametrized: typing.OrderedDict[int, str],
|
||||
):
|
||||
reveal_type(list_bare) # revealed: list
|
||||
reveal_type(list_parametrized) # revealed: list
|
||||
|
||||
reveal_type(dict_bare) # revealed: dict
|
||||
reveal_type(dict_parametrized) # revealed: dict
|
||||
|
||||
reveal_type(set_bare) # revealed: set
|
||||
reveal_type(set_parametrized) # revealed: set
|
||||
|
||||
reveal_type(frozen_set_bare) # revealed: frozenset
|
||||
reveal_type(frozen_set_parametrized) # revealed: frozenset
|
||||
|
||||
reveal_type(chain_map_bare) # revealed: ChainMap
|
||||
reveal_type(chain_map_parametrized) # revealed: ChainMap
|
||||
|
||||
reveal_type(counter_bare) # revealed: Counter
|
||||
reveal_type(counter_parametrized) # revealed: Counter
|
||||
|
||||
reveal_type(default_dict_bare) # revealed: defaultdict
|
||||
reveal_type(default_dict_parametrized) # revealed: defaultdict
|
||||
|
||||
reveal_type(deque_bare) # revealed: deque
|
||||
reveal_type(deque_parametrized) # revealed: deque
|
||||
|
||||
reveal_type(ordered_dict_bare) # revealed: OrderedDict
|
||||
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
The aliases can be inherited from. Some of these are still partially or wholly TODOs.
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
####################
|
||||
### Built-ins
|
||||
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
|
||||
reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
|
||||
reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
####################
|
||||
### `collections`
|
||||
|
||||
class ChainMapSubclass(typing.ChainMap): ...
|
||||
|
||||
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
|
||||
reveal_type(ChainMapSubclass.__mro__)
|
||||
|
||||
class CounterSubclass(typing.Counter): ...
|
||||
|
||||
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]]
|
||||
reveal_type(CounterSubclass.__mro__)
|
||||
|
||||
class DefaultDictSubclass(typing.DefaultDict): ...
|
||||
|
||||
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]]
|
||||
reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]]
|
||||
reveal_type(OrderedDictSubclass.__mro__)
|
||||
```
|
||||
@@ -3,75 +3,56 @@
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f() -> "int":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
def f(v: "int"):
|
||||
reveal_type(v) # revealed: int
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
def f() -> "'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
def f(v: "'int'"):
|
||||
reveal_type(v) # revealed: int
|
||||
```
|
||||
|
||||
## Type expression
|
||||
|
||||
```py
|
||||
def f1() -> "int | str":
|
||||
return 1
|
||||
|
||||
def f2() -> "tuple[int, str]":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: int | str
|
||||
reveal_type(f2()) # revealed: tuple[int, str]
|
||||
def f1(v: "int | str", w: "tuple[int, str]"):
|
||||
reveal_type(v) # revealed: int | str
|
||||
reveal_type(w) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Partial
|
||||
|
||||
```py
|
||||
def f() -> tuple[int, "str"]:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: tuple[int, str]
|
||||
def f(v: tuple[int, "str"]):
|
||||
reveal_type(v) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Deferred
|
||||
|
||||
```py
|
||||
def f() -> "Foo":
|
||||
return Foo()
|
||||
def f(v: "Foo"):
|
||||
reveal_type(v) # revealed: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Foo
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## Deferred (undefined)
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
def f() -> "Foo":
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Unknown
|
||||
def f(v: "Foo"):
|
||||
reveal_type(v) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Partial deferred
|
||||
|
||||
```py
|
||||
def f() -> int | "Foo":
|
||||
return 1
|
||||
def f(v: int | "Foo"):
|
||||
reveal_type(v) # revealed: int | Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: int | Foo
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## `typing.Literal`
|
||||
@@ -79,65 +60,43 @@ reveal_type(f()) # revealed: int | Foo
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f1() -> Literal["Foo", "Bar"]:
|
||||
return "Foo"
|
||||
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
|
||||
reveal_type(v) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(w) # revealed: Literal["Foo", "Bar"]
|
||||
|
||||
def f2() -> 'Literal["Foo", "Bar"]':
|
||||
return "Foo"
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## Various string kinds
|
||||
|
||||
```py
|
||||
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
|
||||
def f1() -> r"int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-f-string] "Type expressions cannot use f-strings"
|
||||
def f2() -> f"int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
def f3() -> b"int":
|
||||
return 1
|
||||
|
||||
def f4() -> "int":
|
||||
return 1
|
||||
|
||||
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
|
||||
def f5() -> "in" "t":
|
||||
return 1
|
||||
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
def f6() -> "\N{LATIN SMALL LETTER I}nt":
|
||||
return 1
|
||||
|
||||
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
|
||||
def f7() -> "\x69nt":
|
||||
return 1
|
||||
|
||||
def f8() -> """int""":
|
||||
return 1
|
||||
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
def f9() -> "b'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: Unknown
|
||||
reveal_type(f2()) # revealed: Unknown
|
||||
reveal_type(f3()) # revealed: Unknown
|
||||
reveal_type(f4()) # revealed: int
|
||||
reveal_type(f5()) # revealed: Unknown
|
||||
reveal_type(f6()) # revealed: Unknown
|
||||
reveal_type(f7()) # revealed: Unknown
|
||||
reveal_type(f8()) # revealed: int
|
||||
reveal_type(f9()) # revealed: Unknown
|
||||
def f1(
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
a: r"int",
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
b: f"int",
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
c: b"int",
|
||||
d: "int",
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
e: "in" "t",
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
f: "\N{LATIN SMALL LETTER I}nt",
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
g: "\x69nt",
|
||||
h: """int""",
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
i: "b'int'",
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: int
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(h) # revealed: int
|
||||
reveal_type(i) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Various string kinds in `typing.Literal`
|
||||
@@ -145,10 +104,8 @@ reveal_type(f9()) # revealed: Unknown
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
|
||||
return "normal"
|
||||
|
||||
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
||||
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
@@ -175,8 +132,7 @@ c: "Foo"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
|
||||
d: "Foo" = 1
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
class Foo: ...
|
||||
|
||||
c = Foo()
|
||||
|
||||
@@ -208,9 +164,9 @@ i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
m: "yield 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Unsupported special forms
|
||||
|
||||
## Not yet supported
|
||||
|
||||
Several special forms are unsupported by red-knot currently. However, we also don't emit
|
||||
false-positive errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar
|
||||
|
||||
P = ParamSpec("P")
|
||||
Ts = TypeVarTuple("Ts")
|
||||
R_co = TypeVar("R_co", covariant=True)
|
||||
|
||||
Alias: TypeAlias = int
|
||||
|
||||
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
|
||||
return callback(42, *args, **kwargs)
|
||||
|
||||
class Foo:
|
||||
def method(self, x: Self):
|
||||
reveal_type(x) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from most of these. `typing.Callable` is an exception.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
|
||||
|
||||
class A(Self): ... # error: [invalid-base]
|
||||
class B(Unpack): ... # error: [invalid-base]
|
||||
class C(TypeGuard): ... # error: [invalid-base]
|
||||
class D(TypeIs): ... # error: [invalid-base]
|
||||
class E(Concatenate): ... # error: [invalid-base]
|
||||
class F(Callable): ...
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
||||
```
|
||||
|
||||
## Subscriptability
|
||||
|
||||
Some of these are not subscriptable:
|
||||
|
||||
```py
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
X: TypeAlias[T] = int # error: [invalid-type-form]
|
||||
|
||||
class Foo[T]:
|
||||
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
|
||||
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
|
||||
def method(self: Self[int]) -> Self[int]:
|
||||
reveal_type(self) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
# Unsupported type qualifiers
|
||||
|
||||
## Not yet supported
|
||||
|
||||
Several type qualifiers are unsupported by red-knot currently. However, we also don't emit
|
||||
false-positive errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly, TypedDict
|
||||
|
||||
X: Final = 42
|
||||
Y: Final[int] = 42
|
||||
|
||||
class Foo:
|
||||
A: ClassVar[int] = 42
|
||||
|
||||
# TODO: `TypedDict` is actually valid as a base
|
||||
# error: [invalid-base]
|
||||
class Bar(TypedDict):
|
||||
x: Required[int]
|
||||
y: NotRequired[str]
|
||||
z: ReadOnly[bytes]
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from a type qualifier.
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||
|
||||
class A(Final): ... # error: [invalid-base]
|
||||
class B(ClassVar): ... # error: [invalid-base]
|
||||
class C(Required): ... # error: [invalid-base]
|
||||
class D(NotRequired): ... # error: [invalid-base]
|
||||
class E(ReadOnly): ... # error: [invalid-base]
|
||||
```
|
||||
@@ -78,20 +78,10 @@ c: tuple[str | int, str] = ([], "foo")
|
||||
## PEP-604 annotations are supported
|
||||
|
||||
```py
|
||||
def foo() -> str | int | None:
|
||||
return None
|
||||
|
||||
reveal_type(foo()) # revealed: str | int | None
|
||||
|
||||
def bar() -> str | str | None:
|
||||
return None
|
||||
|
||||
reveal_type(bar()) # revealed: str | None
|
||||
|
||||
def baz() -> str | str:
|
||||
return "Hello, world!"
|
||||
|
||||
reveal_type(baz()) # revealed: str
|
||||
def foo(v: str | int | None, w: str | str | None, x: str | str):
|
||||
reveal_type(v) # revealed: str | int | None
|
||||
reveal_type(w) # revealed: str | None
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## Attribute expressions in type annotations are understood
|
||||
@@ -118,8 +108,7 @@ from __future__ import annotations
|
||||
|
||||
x: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
class Foo: ...
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
@@ -130,8 +119,7 @@ reveal_type(x) # revealed: Foo
|
||||
```pyi path=main.pyi
|
||||
x: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
class Foo: ...
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
|
||||
@@ -49,134 +49,116 @@ reveal_type(x) # revealed: int
|
||||
## Method union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
else:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
flag = bool_instance()
|
||||
f = Foo()
|
||||
f += 12
|
||||
|
||||
class Foo:
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
else:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | int
|
||||
reveal_type(f) # revealed: str | int
|
||||
```
|
||||
|
||||
## Partially bound `__iadd__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
class Foo:
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
f = Foo()
|
||||
|
||||
f = Foo()
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Partially bound with `__add__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: str) -> str:
|
||||
return "Hello, world!"
|
||||
if flag:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: str) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | str
|
||||
reveal_type(f) # revealed: int | str
|
||||
```
|
||||
|
||||
## Partially bound target union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if flag1:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
if flag2:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
if bool_instance():
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Target union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
class Foo:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | float
|
||||
reveal_type(f) # revealed: str | float
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def f(flag: bool, flag2: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if flag:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
flag = bool_instance()
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
if flag2:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
# Class attributes
|
||||
# Attributes
|
||||
|
||||
Tests for attribute access on various kinds of types.
|
||||
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
else:
|
||||
class C1:
|
||||
x = 2
|
||||
|
||||
class C2:
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x = 3
|
||||
else:
|
||||
x = 4
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
else:
|
||||
class C1:
|
||||
x = 2
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
x = 3
|
||||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited attributes
|
||||
@@ -68,24 +66,19 @@ reveal_type(A.X) # revealed: Literal[42]
|
||||
In this example, the `x` attribute is not defined in the `C2` element of the union:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C1:
|
||||
x = 1
|
||||
class C2: ...
|
||||
|
||||
class C2: ...
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
@@ -94,26 +87,21 @@ We raise the same diagnostic if the attribute is possibly-unbound in at least on
|
||||
union:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C1:
|
||||
x = 1
|
||||
class C2:
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
class C2:
|
||||
if bool_instance():
|
||||
x = 2
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## Unions with all paths unbound
|
||||
@@ -121,16 +109,115 @@ reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
If the symbol is unbound in all elements of the union, we detect that:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class C1: ...
|
||||
class C2: ...
|
||||
C = C1 if flag else C2
|
||||
|
||||
class C1: ...
|
||||
class C2: ...
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
C = C1 if flag else C2
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Objects of all types have a `__class__` method
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
|
||||
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
a = 42
|
||||
reveal_type(a.__class__) # revealed: Literal[int]
|
||||
|
||||
b = "42"
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
|
||||
c = b"42"
|
||||
reveal_type(c.__class__) # revealed: Literal[bytes]
|
||||
|
||||
d = True
|
||||
reveal_type(d.__class__) # revealed: Literal[bool]
|
||||
|
||||
e = (42, 42)
|
||||
reveal_type(e.__class__) # revealed: Literal[tuple]
|
||||
|
||||
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
reveal_type(a.__class__) # revealed: type[int]
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
reveal_type(c.__class__) # revealed: type[int] | type[str]
|
||||
|
||||
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
|
||||
# It would be incorrect to infer `Literal[type]` here,
|
||||
# as `c` could be some subclass of `str` with a custom metaclass.
|
||||
# All we know is that the metaclass must be a (non-strict) subclass of `type`.
|
||||
reveal_type(d.__class__) # revealed: type[type]
|
||||
|
||||
reveal_type(f.__class__) # revealed: Literal[FunctionType]
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Function-literal attributes
|
||||
|
||||
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
|
||||
functions are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__defaults__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
|
||||
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
```
|
||||
|
||||
## Int-literal attributes
|
||||
|
||||
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
|
||||
integers are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
reveal_type((2).bit_length) # revealed: @Todo(instance attributes)
|
||||
reveal_type((2).denominator) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
reveal_type((2).numerator) # revealed: Literal[2]
|
||||
reveal_type((2).real) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Literal `bool` attributes
|
||||
|
||||
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
|
||||
bols are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
reveal_type(True.__and__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(False.__or__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
reveal_type(True.numerator) # revealed: Literal[1]
|
||||
reveal_type(False.real) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
## Bytes-literal attributes
|
||||
|
||||
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
|
||||
|
||||
```py
|
||||
reveal_type(b"foo".join) # revealed: @Todo(instance attributes)
|
||||
reveal_type(b"foo".endswith) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
@@ -281,20 +281,12 @@ reveal_type(42 + 4.2) # revealed: int
|
||||
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3 + 3j) # revealed: int
|
||||
|
||||
def returns_int() -> int:
|
||||
return 42
|
||||
def _(x: bool, y: int):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: float
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
x = returns_bool()
|
||||
y = returns_int()
|
||||
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
```
|
||||
|
||||
## With literal types
|
||||
|
||||
@@ -7,29 +7,25 @@ Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand
|
||||
evaluated.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if bool_instance() or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if bool_instance() and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if flag and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## First expression is always evaluated
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if (x := 1) or flag:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if (x := 1) or bool_instance():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if (x := 1) and bool_instance():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if (x := 1) and flag:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Statically known truthiness
|
||||
@@ -49,30 +45,26 @@ if True and (x := 1):
|
||||
## Later expressions can always use variables from earlier expressions
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
flag or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Nested expressions
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1 or ((x := 1) and flag2):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and flag1) or flag2:
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
|
||||
if bool_instance() or ((x := 1) and bool_instance()):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and bool_instance()) or bool_instance():
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
if (flag1 and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -22,29 +22,27 @@ reveal_type(b) # revealed: Unknown
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
class PossiblyNotCallable:
|
||||
if flag:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
class PossiblyNotCallable:
|
||||
if flag():
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
if flag():
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
@@ -61,15 +59,14 @@ reveal_type(a()) # revealed: Unknown
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
class NonCallable:
|
||||
if flag:
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
class NonCallable:
|
||||
if flag():
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[__call__] | Literal[1]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
@@ -57,12 +57,10 @@ x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def foo() -> int:
|
||||
return 42
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -3,22 +3,14 @@
|
||||
## Union of return types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
else:
|
||||
|
||||
def f() -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(f()) # revealed: int | str
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def f() -> int:
|
||||
return 1
|
||||
else:
|
||||
def f() -> str:
|
||||
return "foo"
|
||||
reveal_type(f()) # revealed: int | str
|
||||
```
|
||||
|
||||
## Calling with an unknown union
|
||||
@@ -26,13 +18,10 @@ reveal_type(f()) # revealed: int | str
|
||||
```py
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
if coinflip():
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
@@ -44,20 +33,14 @@ reveal_type(f()) # revealed: Unknown | int
|
||||
Calling a union with a non-callable element should emit a diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
@@ -65,23 +48,17 @@ reveal_type(x) # revealed: Unknown | int
|
||||
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = "foo"
|
||||
else:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
def _(flag: bool, flag2: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = "foo"
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
## All non-callable union elements
|
||||
@@ -89,16 +66,12 @@ reveal_type(f())
|
||||
Calling a union with no callable elements can emit a simpler diagnostic.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,38 +3,31 @@
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_a() -> A: ...
|
||||
def get_object() -> object: ...
|
||||
def _(a1: A, a2: A, o: object):
|
||||
n1 = None
|
||||
n2 = None
|
||||
|
||||
a1 = get_a()
|
||||
a2 = get_a()
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
|
||||
n1 = None
|
||||
n2 = None
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
|
||||
o = get_object()
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -312,17 +312,9 @@ reveal_type(1 <= 2j) # revealed: bool
|
||||
reveal_type(1 > 2j) # revealed: bool
|
||||
reveal_type(1 >= 2j) # revealed: bool
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
x = bool_instance()
|
||||
y = int_instance()
|
||||
|
||||
reveal_type(x < y) # revealed: bool
|
||||
reveal_type(y < x) # revealed: bool
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
def f(x: bool, y: int):
|
||||
reveal_type(x < y) # revealed: bool
|
||||
reveal_type(y < x) # revealed: bool
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -20,10 +20,8 @@ reveal_type(1 <= "" and 0 < 1) # revealed: bool
|
||||
|
||||
```py
|
||||
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(1 == int_instance()) # revealed: bool
|
||||
reveal_type(9 < int_instance()) # revealed: bool
|
||||
reveal_type(int_instance() < int_instance()) # revealed: bool
|
||||
def _(a: int, b: int):
|
||||
reveal_type(1 == a) # revealed: bool
|
||||
reveal_type(9 < a) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -14,21 +14,19 @@ class Child1(Base):
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def get_base() -> Base: ...
|
||||
def _(x: Base):
|
||||
c1 = Child1()
|
||||
|
||||
x = get_base()
|
||||
c1 = Child1()
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
@@ -73,18 +71,15 @@ if x != "abc":
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def get_int() -> int: ...
|
||||
def _(x: int):
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
x = get_int()
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
@@ -92,18 +87,15 @@ if x != 1:
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
def _(o: object):
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
o = object()
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
@@ -119,16 +111,13 @@ class Container:
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
@@ -142,14 +131,11 @@ class Container:
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
## String literals
|
||||
|
||||
```py
|
||||
def str_instance() -> str: ...
|
||||
def _(x: str):
|
||||
reveal_type("abc" == "abc") # revealed: Literal[True]
|
||||
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
|
||||
reveal_type("abc" in "ab cd") # revealed: Literal[False]
|
||||
reveal_type("" not in "hello") # revealed: Literal[False]
|
||||
reveal_type("--" is "--") # revealed: bool
|
||||
reveal_type("A" is "B") # revealed: Literal[False]
|
||||
reveal_type("--" is not "--") # revealed: bool
|
||||
reveal_type("A" is not "B") # revealed: Literal[True]
|
||||
reveal_type(x < "...") # revealed: bool
|
||||
|
||||
reveal_type("abc" == "abc") # revealed: Literal[True]
|
||||
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
|
||||
reveal_type("abc" in "ab cd") # revealed: Literal[False]
|
||||
reveal_type("" not in "hello") # revealed: Literal[False]
|
||||
reveal_type("--" is "--") # revealed: bool
|
||||
reveal_type("A" is "B") # revealed: Literal[False]
|
||||
reveal_type("--" is not "--") # revealed: bool
|
||||
reveal_type("A" is not "B") # revealed: Literal[True]
|
||||
reveal_type(str_instance() < "...") # revealed: bool
|
||||
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -58,28 +58,23 @@ reveal_type(c >= d) # revealed: Literal[True]
|
||||
#### Results with Ambiguity
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(x: bool, y: int):
|
||||
a = (x,)
|
||||
b = (y,)
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
a = (bool_instance(),)
|
||||
b = (int_instance(),)
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
```
|
||||
|
||||
#### Comparison Unsupported
|
||||
@@ -197,7 +192,7 @@ reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
|
||||
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
|
||||
> Example: `(<int instance>, "foo") == (<int instance>, "bar")`
|
||||
|
||||
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
|
||||
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
|
||||
@@ -208,42 +203,38 @@ In contrast, with operators like `<` and `>`, the comparison must consider each
|
||||
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
|
||||
|
||||
```py
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
def _(x: str, y: int):
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False]
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
a = (x, y, "foo")
|
||||
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
a = (str_instance(), int_instance(), "foo")
|
||||
b = (x, y, "bar")
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
b = (str_instance(), int_instance(), "bar")
|
||||
c = (x, y, "foo", "different_length")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
c = (str_instance(), int_instance(), "foo", "different_length")
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
```
|
||||
|
||||
#### Error Propagation
|
||||
@@ -252,42 +243,36 @@ Errors occurring within a tuple comparison should propagate outward. However, if
|
||||
comparison can clearly conclude before encountering an error, the error should not be raised.
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
def _(n: int, s: str):
|
||||
class A: ...
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
a = (0, n, A())
|
||||
|
||||
class A: ...
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, n, A())
|
||||
|
||||
a = (0, int_instance(), A())
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, int_instance(), A())
|
||||
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Membership Test Comparisons
|
||||
@@ -295,22 +280,20 @@ reveal_type(a >= b) # revealed: Literal[False]
|
||||
"Membership Test Comparisons" refers to the operators `in` and `not in`.
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
def _(n: int):
|
||||
a = (1, 2)
|
||||
b = ((3, 4), (1, 2))
|
||||
c = ((1, 2, 3), (4, 5, 6))
|
||||
d = ((n, n), (n, n))
|
||||
|
||||
a = (1, 2)
|
||||
b = ((3, 4), (1, 2))
|
||||
c = ((1, 2, 3), (4, 5, 6))
|
||||
d = ((int_instance(), int_instance()), (int_instance(), int_instance()))
|
||||
reveal_type(a in b) # revealed: Literal[True]
|
||||
reveal_type(a not in b) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a in b) # revealed: Literal[True]
|
||||
reveal_type(a not in b) # revealed: Literal[False]
|
||||
reveal_type(a in c) # revealed: Literal[False]
|
||||
reveal_type(a not in c) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a in c) # revealed: Literal[False]
|
||||
reveal_type(a not in c) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a in d) # revealed: bool
|
||||
reveal_type(a not in d) # revealed: bool
|
||||
reveal_type(a in d) # revealed: bool
|
||||
reveal_type(a not in d) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity Comparisons
|
||||
|
||||
@@ -5,49 +5,46 @@
|
||||
Comparisons on union types need to consider all possible cases:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
one_or_two = 1 if flag else 2
|
||||
|
||||
flag = bool_instance()
|
||||
one_or_two = 1 if flag else 2
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
reveal_type(one_or_two <= 1) # revealed: bool
|
||||
reveal_type(one_or_two <= 0) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
reveal_type(one_or_two <= 1) # revealed: bool
|
||||
reveal_type(one_or_two <= 0) # revealed: Literal[False]
|
||||
reveal_type(2 >= one_or_two) # revealed: Literal[True]
|
||||
reveal_type(1 >= one_or_two) # revealed: bool
|
||||
reveal_type(0 >= one_or_two) # revealed: Literal[False]
|
||||
|
||||
reveal_type(2 >= one_or_two) # revealed: Literal[True]
|
||||
reveal_type(1 >= one_or_two) # revealed: bool
|
||||
reveal_type(0 >= one_or_two) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 1) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 2) # revealed: bool
|
||||
reveal_type(one_or_two < 3) # revealed: Literal[True]
|
||||
|
||||
reveal_type(one_or_two < 1) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 2) # revealed: bool
|
||||
reveal_type(one_or_two < 3) # revealed: Literal[True]
|
||||
reveal_type(one_or_two > 0) # revealed: Literal[True]
|
||||
reveal_type(one_or_two > 1) # revealed: bool
|
||||
reveal_type(one_or_two > 2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two > 0) # revealed: Literal[True]
|
||||
reveal_type(one_or_two > 1) # revealed: bool
|
||||
reveal_type(one_or_two > 2) # revealed: Literal[False]
|
||||
reveal_type(one_or_two == 3) # revealed: Literal[False]
|
||||
reveal_type(one_or_two == 1) # revealed: bool
|
||||
|
||||
reveal_type(one_or_two == 3) # revealed: Literal[False]
|
||||
reveal_type(one_or_two == 1) # revealed: bool
|
||||
reveal_type(one_or_two != 3) # revealed: Literal[True]
|
||||
reveal_type(one_or_two != 1) # revealed: bool
|
||||
|
||||
reveal_type(one_or_two != 3) # revealed: Literal[True]
|
||||
reveal_type(one_or_two != 1) # revealed: bool
|
||||
a_or_ab = "a" if flag else "ab"
|
||||
|
||||
a_or_ab = "a" if flag else "ab"
|
||||
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
|
||||
reveal_type("a" in a_or_ab) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
|
||||
reveal_type("a" in a_or_ab) # revealed: Literal[True]
|
||||
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
|
||||
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
|
||||
|
||||
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
|
||||
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
|
||||
reveal_type("b" in a_or_ab) # revealed: bool
|
||||
reveal_type("b" not in a_or_ab) # revealed: bool
|
||||
|
||||
reveal_type("b" in a_or_ab) # revealed: bool
|
||||
reveal_type("b" not in a_or_ab) # revealed: bool
|
||||
one_or_none = 1 if flag else None
|
||||
|
||||
one_or_none = 1 if flag else None
|
||||
|
||||
reveal_type(one_or_none is None) # revealed: bool
|
||||
reveal_type(one_or_none is not None) # revealed: bool
|
||||
reveal_type(one_or_none is None) # revealed: bool
|
||||
reveal_type(one_or_none is not None) # revealed: bool
|
||||
```
|
||||
|
||||
## Union on both sides of the comparison
|
||||
@@ -56,18 +53,15 @@ With unions on both sides, we need to consider the full cross product of options
|
||||
resulting (union) type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag_s: bool, flag_l: bool):
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
|
||||
flag_s, flag_l = bool_instance(), bool_instance()
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
reveal_type(small <= large) # revealed: Literal[True]
|
||||
reveal_type(small >= large) # revealed: bool
|
||||
|
||||
reveal_type(small <= large) # revealed: Literal[True]
|
||||
reveal_type(small >= large) # revealed: bool
|
||||
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Unsupported operations
|
||||
@@ -77,12 +71,9 @@ back to `bool` for the result type instead of trying to infer something more pre
|
||||
(supported) variants:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = [1, 2] if flag else 1
|
||||
|
||||
flag = bool_instance()
|
||||
x = [1, 2] if flag else 1
|
||||
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
reveal_type(result) # revealed: bool
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
reveal_type(result) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
# Comparison: Unsupported operators
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
class A: ...
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `object` and `int`")
|
||||
c = object() < 5
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(c) # revealed: bool
|
||||
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `int` and `object`")
|
||||
d = 5 < object()
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(d) # revealed: bool
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `object` and `int`")
|
||||
c = object() < 5
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(c) # revealed: bool
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `int` and `object`")
|
||||
d = 5 < object()
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(d) # revealed: bool
|
||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
|
||||
flag = bool_instance()
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (bool_instance(), A()) < (bool_instance(), A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (flag1, A()) < (flag2, A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,47 +3,35 @@
|
||||
## Simple if-expression
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## If-expression with walrus operator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
reveal_type(z) # revealed: Literal[0, 2]
|
||||
def _(flag: bool):
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
reveal_type(z) # revealed: Literal[0, 2]
|
||||
```
|
||||
|
||||
## Nested if-expression
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
def _(flag: bool, flag2: bool):
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
@@ -3,128 +3,115 @@
|
||||
## Simple if
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
flag = bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Simple if-elif-else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
else:
|
||||
r = y
|
||||
y = 5
|
||||
s = y
|
||||
x = y
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
else:
|
||||
r = y
|
||||
y = 5
|
||||
s = y
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
||||
## Single symbol across if-elif-else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag2: bool):
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
y = 3
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## if-elif-else without else assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## if-elif-else with intervening assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
z = 3
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
if flag:
|
||||
y = 1
|
||||
z = 3
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## Nested if statement
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
if flag2:
|
||||
y = 1
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
if flag:
|
||||
if flag2:
|
||||
y = 1
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## if-elif without else
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
@@ -3,39 +3,43 @@
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Without wildcard
|
||||
|
||||
```py
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
## Basic match
|
||||
|
||||
```py
|
||||
y = 1
|
||||
y = 2
|
||||
match 0:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
def _(target: int):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
match target:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
@@ -10,42 +10,66 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
|
||||
## Incompatible declarations
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Partial declarations
|
||||
## Incompatible declarations for 2 (out of 3) types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1:
|
||||
x: str
|
||||
elif flag2:
|
||||
x: int
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
# Here, the declared type for `x` is `int | str | Unknown`.
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations with bad assignment
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b"foo"
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b"foo"
|
||||
```
|
||||
|
||||
## No errors
|
||||
|
||||
Currently, we avoid raising the conflicting-declarations for the following cases:
|
||||
|
||||
### Partial declarations
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: int
|
||||
|
||||
x = 1
|
||||
```
|
||||
|
||||
### Partial declarations in try-except
|
||||
|
||||
Refer to <https://github.com/astral-sh/ruff/issues/13966>
|
||||
|
||||
```py
|
||||
def _():
|
||||
try:
|
||||
x: int = 1
|
||||
except:
|
||||
x = 2
|
||||
|
||||
x = 3
|
||||
```
|
||||
|
||||
@@ -49,12 +49,124 @@ def foo(
|
||||
try:
|
||||
help()
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo(exception type)
|
||||
reveal_type(e) # revealed: AttributeError
|
||||
except y as f:
|
||||
# TODO: should be `OSError | RuntimeError`
|
||||
reveal_type(f) # revealed: @Todo(exception type)
|
||||
reveal_type(f) # revealed: OSError | RuntimeError
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo(exception type)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
## Invalid exception handlers
|
||||
|
||||
```py
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except 3 as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except (ValueError, OSError, "foo", b"bar") as e:
|
||||
reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
|
||||
def foo(
|
||||
x: type[str],
|
||||
y: tuple[type[OSError], type[RuntimeError], int],
|
||||
z: tuple[type[str], ...],
|
||||
):
|
||||
try:
|
||||
help()
|
||||
# error: [invalid-exception-caught]
|
||||
except x as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
# error: [invalid-exception-caught]
|
||||
except y as f:
|
||||
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
except z as g:
|
||||
# TODO: should emit a diagnostic here:
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
## Object raised is not an exception
|
||||
|
||||
```py
|
||||
try:
|
||||
raise AttributeError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise FloatingPointError # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise 1 # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise int # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise e # fine
|
||||
|
||||
def _(e: Exception | type[Exception] | None):
|
||||
raise e # error: [invalid-raise]
|
||||
```
|
||||
|
||||
## Exception cause is not an exception
|
||||
|
||||
```py
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise ModuleNotFoundError from e # fine
|
||||
|
||||
def _(e: Exception | type[Exception] | None):
|
||||
raise IndexError from e # fine
|
||||
|
||||
def _(e: int | None):
|
||||
raise IndexError from e # error: [invalid-raise]
|
||||
```
|
||||
|
||||
@@ -1,30 +1,66 @@
|
||||
# Except star
|
||||
# `except*`
|
||||
|
||||
## Except\* with BaseException
|
||||
`except*` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
## `except*` with `BaseException`
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* BaseException as e:
|
||||
# TODO: should be `BaseExceptionGroup[BaseException]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## Except\* with specific exception
|
||||
## `except*` with specific exception
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* OSError as e:
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
|
||||
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
|
||||
# (needs homogenous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## Except\* with multiple exceptions
|
||||
## `except*` with multiple exceptions
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* (TypeError, AttributeError) as e:
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
|
||||
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
|
||||
# (needs homogenous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## `except*` with mix of `Exception`s and `BaseException`s
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* (KeyboardInterrupt, AttributeError) as e:
|
||||
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## Invalid `except*` handlers
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* 3 as e: # error: [invalid-exception-caught]
|
||||
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
|
||||
try:
|
||||
help()
|
||||
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
|
||||
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
@@ -9,5 +9,4 @@ try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
```
|
||||
|
||||
@@ -3,26 +3,25 @@
|
||||
## Boundness
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
always_bound = 1
|
||||
|
||||
class A:
|
||||
always_bound = 1
|
||||
if flag:
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
|
||||
if flag():
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
if flag:
|
||||
possibly_unbound = "abc"
|
||||
|
||||
if flag():
|
||||
possibly_unbound = "abc"
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,54 +3,45 @@
|
||||
## OR
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
reveal_type(True or False) # revealed: Literal[True]
|
||||
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
|
||||
reveal_type("" or "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo() or False) # revealed: str | Literal[False]
|
||||
reveal_type(foo() or True) # revealed: str | Literal[True]
|
||||
def _(foo: str):
|
||||
reveal_type(True or False) # revealed: Literal[True]
|
||||
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
|
||||
reveal_type("" or "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo or False) # revealed: str | Literal[False]
|
||||
reveal_type(foo or True) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo() and False) # revealed: str | Literal[False]
|
||||
reveal_type(foo() and True) # revealed: str | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
def _(foo: str):
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo and False) # revealed: str | Literal[False]
|
||||
reveal_type(foo and True) # revealed: str | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Simple function calls to bool
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x = True
|
||||
else:
|
||||
x = False
|
||||
|
||||
if returns_bool():
|
||||
x = True
|
||||
else:
|
||||
x = False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(x) # revealed: bool
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
reveal_type("x" and "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type("x" or "y" and "z") # revealed: Literal["x"]
|
||||
reveal_type("" and "y" or "z") # revealed: Literal["z"]
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
## Union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
|
||||
def _(flag: bool):
|
||||
reveal_type(1 if flag else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
@@ -30,14 +28,12 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x: Literal[42, "hello"] = 42 if flag else "hello"
|
||||
|
||||
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
```
|
||||
|
||||
@@ -211,8 +211,7 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
### No `__len__`
|
||||
|
||||
```py
|
||||
class NoDunderLen:
|
||||
pass
|
||||
class NoDunderLen: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
|
||||
31
crates/red_knot_python_semantic/resources/mdtest/final.md
Normal file
31
crates/red_knot_python_semantic/resources/mdtest/final.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Tests for the `@typing(_extensions).final` decorator
|
||||
|
||||
## Cannot subclass
|
||||
|
||||
Don't do this:
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class A: ...
|
||||
|
||||
class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`"
|
||||
|
||||
@typing_extensions.final
|
||||
class C: ...
|
||||
|
||||
class D(C): ... # error: [subclass-of-final-class]
|
||||
class E: ...
|
||||
class F: ...
|
||||
class G: ...
|
||||
|
||||
# fmt: off
|
||||
class H(
|
||||
E,
|
||||
F,
|
||||
A, # error: [subclass-of-final-class]
|
||||
G,
|
||||
): ...
|
||||
```
|
||||
@@ -73,7 +73,7 @@ def f[T]():
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -25,3 +25,82 @@ reveal_type(D) # revealed: Literal[C]
|
||||
```py path=b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
import a.b
|
||||
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Deeply nested
|
||||
|
||||
```py
|
||||
import a.b.c
|
||||
|
||||
reveal_type(a.b.c.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/c.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Nested with rename
|
||||
|
||||
```py
|
||||
import a.b as b
|
||||
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Deeply nested with rename
|
||||
|
||||
```py
|
||||
import a.b.c as c
|
||||
|
||||
reveal_type(c.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b/c.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Unresolvable submodule imports
|
||||
|
||||
```py
|
||||
# Topmost component resolvable, submodule not resolvable:
|
||||
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
|
||||
# Topmost component unresolvable:
|
||||
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
```py
|
||||
import builtins
|
||||
|
||||
x = builtins.copyright
|
||||
reveal_type(x) # revealed: Literal[copyright]
|
||||
x = builtins.chr
|
||||
reveal_type(x) # revealed: Literal[chr]
|
||||
```
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
## Maybe unbound
|
||||
|
||||
```py path=maybe_unbound.py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
if coinflip():
|
||||
y = 3
|
||||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
@@ -31,13 +30,12 @@ reveal_type(y) # revealed: Literal[3]
|
||||
## Maybe unbound annotated
|
||||
|
||||
```py path=maybe_unbound_annotated.py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
if coinflip():
|
||||
y: int = 3
|
||||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Literal[3]
|
||||
@@ -63,10 +61,10 @@ reveal_type(y) # revealed: int
|
||||
Importing a possibly undeclared name still gives us its declared type:
|
||||
|
||||
```py path=maybe_undeclared.py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
if coinflip():
|
||||
x: int
|
||||
```
|
||||
|
||||
@@ -83,14 +81,12 @@ def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
if coinflip():
|
||||
from c import f
|
||||
else:
|
||||
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
@@ -111,11 +107,10 @@ x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
if coinflip():
|
||||
from c import x
|
||||
else:
|
||||
x = 1
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Conflicting attributes and submodules
|
||||
|
||||
## Via import
|
||||
|
||||
```py
|
||||
import a.b
|
||||
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
## Via from/import
|
||||
|
||||
```py
|
||||
from a import b
|
||||
|
||||
reveal_type(b) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
## Via both
|
||||
|
||||
```py
|
||||
import a.b
|
||||
from a import b
|
||||
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
## Via both (backwards)
|
||||
|
||||
In this test, we infer a different type for `b` than the runtime behavior of the Python interpreter.
|
||||
The interpreter will not load the submodule `a.b` during the `from a import b` statement, since `a`
|
||||
contains a non-module attribute named `b`. (See the [definition][from-import] of a `from...import`
|
||||
statement for details.) However, because our import tracking is flow-insensitive, we will see that
|
||||
`a.b` is imported somewhere in the file, and therefore assume that the `from...import` statement
|
||||
sees the submodule as the value of `b` instead of the integer.
|
||||
|
||||
```py
|
||||
from a import b
|
||||
import a.b
|
||||
|
||||
# Python would say `Literal[42]` for `b`
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
b = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
```
|
||||
|
||||
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||
@@ -7,3 +7,25 @@ from import bar # error: [invalid-syntax]
|
||||
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Invalid nested module import
|
||||
|
||||
TODO: This is correctly flagged as an error, but we could clean up the diagnostics that we report.
|
||||
|
||||
```py
|
||||
# TODO: No second diagnostic
|
||||
# error: [invalid-syntax] "Expected ',', found '.'"
|
||||
# error: [unresolved-import] "Module `a` has no member `c`"
|
||||
from a import b.c
|
||||
|
||||
# TODO: Should these be inferred as Unknown?
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.c) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
c = 1
|
||||
```
|
||||
|
||||
@@ -121,23 +121,44 @@ X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
from . import foo
|
||||
|
||||
y = foo.X
|
||||
|
||||
# TODO: should be `Literal[42]`
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(foo.X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Non-existent + bare to module
|
||||
|
||||
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
|
||||
nor an attribute of `package`.
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Import submodule from self
|
||||
|
||||
We don't currently consider `from...import` statements when building up the `imported_modules` set
|
||||
in the semantic index. When accessing an attribute of a module, we only consider it a potential
|
||||
submodule when that submodule name appears in the `imported_modules` set. That means that submodules
|
||||
that are imported via `from...import` are not visible to our type inference if you also access that
|
||||
submodule via the attribute on its parent package.
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import foo
|
||||
import package
|
||||
|
||||
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
|
||||
reveal_type(package.foo.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Tracking imported modules
|
||||
|
||||
These tests depend on how we track which modules have been imported. There are currently two
|
||||
characteristics of our module tracking that can lead to inaccuracies:
|
||||
|
||||
- Imports are tracked on a per-file basis. At runtime, importing a submodule in one file makes that
|
||||
submodule globally available via any reference to the containing package. We will flag an error
|
||||
if a file tries to access a submodule without there being an import of that submodule _in that
|
||||
same file_.
|
||||
|
||||
This is a purposeful decision, and not one we plan to change. If a module wants to re-export some
|
||||
other module that it imports, there are ways to do that (tested below) that are blessed by the
|
||||
typing spec and that are visible to our file-scoped import tracking.
|
||||
|
||||
- Imports are tracked flow-insensitively: submodule accesses are allowed and resolved if that
|
||||
submodule is imported _anywhere in the file_. This handles the common case where all imports are
|
||||
grouped at the top of the file, and is easiest to implement. We might revisit this decision and
|
||||
track submodule imports flow-sensitively, in which case we will have to update the assertions in
|
||||
some of these tests.
|
||||
|
||||
## Import submodule later in file
|
||||
|
||||
This test highlights our flow-insensitive analysis, since we access the `a.b` submodule before it
|
||||
has been imported.
|
||||
|
||||
```py
|
||||
import a
|
||||
|
||||
# Would be an error with flow-sensitive tracking
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
|
||||
import a.b
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Rename a re-export
|
||||
|
||||
This test highlights how import tracking is local to each file, but specifically to the file where a
|
||||
containing module is first referenced. This allows the main module to see that `q.a` contains a
|
||||
submodule `b`, even though `a.b` is never imported in the main module.
|
||||
|
||||
```py
|
||||
from q import a, b
|
||||
|
||||
reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
```py path=q.py
|
||||
import a as a
|
||||
import a.b as b
|
||||
```
|
||||
|
||||
## Attribute overrides submodule
|
||||
|
||||
Technically, either a submodule or a non-module attribute could shadow the other, depending on the
|
||||
ordering of when the submodule is loaded relative to the parent module's `__init__.py` file being
|
||||
evaluated. We have chosen to always have the submodule take priority. (This matches pyright's
|
||||
current behavior, and opposite of mypy's current behavior.)
|
||||
|
||||
```py
|
||||
import sub.b
|
||||
import attr.b
|
||||
|
||||
# In the Python interpreter, `attr.b` is Literal[1]
|
||||
reveal_type(sub.b) # revealed: <module 'sub.b'>
|
||||
reveal_type(attr.b) # revealed: <module 'attr.b'>
|
||||
```
|
||||
|
||||
```py path=sub/__init__.py
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=sub/b.py
|
||||
```
|
||||
|
||||
```py path=attr/__init__.py
|
||||
from . import b as _
|
||||
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=attr/b.py
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# Known constants
|
||||
|
||||
## `typing.TYPE_CHECKING`
|
||||
|
||||
This constant is `True` when in type-checking mode, `False` otherwise. The symbol is defined to be
|
||||
`False` at runtime. In typeshed, it is annotated as `bool`. This test makes sure that we infer
|
||||
`Literal[True]` for it anyways.
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING
|
||||
import typing
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
reveal_type(typing.TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Aliased
|
||||
|
||||
Make sure that we still infer the correct type if the constant has been given a different name:
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING as TC
|
||||
|
||||
reveal_type(TC) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Must originate from `typing`
|
||||
|
||||
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
|
||||
with the same name:
|
||||
|
||||
```py path=constants.py
|
||||
TYPE_CHECKING: bool = False
|
||||
```
|
||||
|
||||
```py
|
||||
from constants import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: bool
|
||||
```
|
||||
|
||||
### `typing_extensions` re-export
|
||||
|
||||
This should behave in the same way as `typing.TYPE_CHECKING`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
@@ -106,23 +106,19 @@ reveal_type(x)
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
flag = bool_instance()
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
# revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
# revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
@@ -160,13 +156,9 @@ class Test2:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int
|
||||
def _(flag: bool):
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterator
|
||||
@@ -215,13 +207,9 @@ class Test2:
|
||||
def __iter__(self) -> TestIter3 | TestIter4:
|
||||
return TestIter3()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
|
||||
def _(flag: bool):
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has no `__iter__` method
|
||||
@@ -235,12 +223,10 @@ class Test:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
def _(flag: bool):
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if flag else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has invalid `__iter__` method
|
||||
@@ -258,12 +244,10 @@ class Test2:
|
||||
def __iter__(self) -> int:
|
||||
return 42
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if coinflip() else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
def _(flag: bool):
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union type as iterator where one union element has no `__next__` method
|
||||
|
||||
@@ -1,59 +1,50 @@
|
||||
# While loops
|
||||
|
||||
## Basic While Loop
|
||||
## Basic `while` loop
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## While with else (no break)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## While with Else (may break)
|
||||
## `while` with `else` (no `break`)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 3
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
x = 2
|
||||
if flag2:
|
||||
y = 4
|
||||
break
|
||||
else:
|
||||
y = x
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## Nested while loops
|
||||
## `while` with `else` (may `break`)
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
x = 2
|
||||
if flag2:
|
||||
y = 4
|
||||
break
|
||||
else:
|
||||
y = x
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
@@ -78,3 +69,50 @@ else:
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
```
|
||||
|
||||
## Boundness
|
||||
|
||||
Make sure that the boundness information is correctly tracked in `while` loop control flow.
|
||||
|
||||
### Basic `while` loop
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
while flag:
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
x
|
||||
```
|
||||
|
||||
### `while` with `else` (no `break`)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
while flag:
|
||||
y = 1
|
||||
else:
|
||||
x = 1
|
||||
|
||||
# no error, `x` is always bound
|
||||
x
|
||||
# error: [possibly-unresolved-reference]
|
||||
y
|
||||
```
|
||||
|
||||
### `while` with `else` (may `break`)
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
while flag:
|
||||
x = 1
|
||||
if flag2:
|
||||
break
|
||||
else:
|
||||
y = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
x
|
||||
# error: [possibly-unresolved-reference]
|
||||
y
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ The following configuration will be attached to the *root* section (without any
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.10"
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
# Basic
|
||||
@@ -34,7 +34,7 @@ Here, we make sure that we can overwrite the global configuration in a child sec
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
@@ -55,7 +55,7 @@ Children in this section should all use the section configuration:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.12"
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
## Child
|
||||
|
||||
@@ -68,7 +68,7 @@ class B(metaclass=M2): ...
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Unknown
|
||||
reveal_type(C.__class__) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## Conflict (2)
|
||||
@@ -85,7 +85,7 @@ class A(metaclass=M1): ...
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
|
||||
class B(A, metaclass=M2): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Unknown
|
||||
reveal_type(B.__class__) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## Common metaclass
|
||||
@@ -129,7 +129,7 @@ class C(metaclass=M12): ...
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class D(A, B, C): ...
|
||||
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
reveal_type(D.__class__) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## Unknown
|
||||
@@ -179,11 +179,11 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
reveal_type(A.__class__) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
@@ -194,3 +194,26 @@ class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Metaclasses of metaclasses
|
||||
|
||||
```py
|
||||
class Foo(type): ...
|
||||
class Bar(type, metaclass=Foo): ...
|
||||
class Baz(type, metaclass=Bar): ...
|
||||
class Spam(metaclass=Baz): ...
|
||||
|
||||
reveal_type(Spam.__class__) # revealed: Literal[Baz]
|
||||
reveal_type(Spam.__class__.__class__) # revealed: Literal[Bar]
|
||||
reveal_type(Spam.__class__.__class__.__class__) # revealed: Literal[Foo]
|
||||
|
||||
def test(x: Spam):
|
||||
reveal_type(x.__class__) # revealed: type[Spam]
|
||||
reveal_type(x.__class__.__class__) # revealed: type[Baz]
|
||||
reveal_type(x.__class__.__class__.__class__) # revealed: type[Bar]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Foo]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__.__class__) # revealed: type[type]
|
||||
|
||||
# revealed: type[type]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__.__class__.__class__.__class__.__class__)
|
||||
```
|
||||
|
||||
@@ -348,14 +348,14 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
|
||||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
@@ -366,9 +366,9 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
|
||||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
@@ -379,9 +379,9 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
||||
|
||||
```py path=a.pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
@@ -391,16 +391,16 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
|
||||
class Spam(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
## Narrowing for `bool(..)` checks
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
|
||||
x = 1 if flag() else None
|
||||
x = 1 if flag else None
|
||||
|
||||
# valid invocation, positive
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# valid invocation, negative
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool(x is not None):
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
# no args/narrowing
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool():
|
||||
# valid invocation, positive
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# invalid invocation, too many positional args
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, 5): # TODO diagnostic
|
||||
# valid invocation, negative
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool(x is not None):
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
# invalid invocation, too many kwargs
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, y=5): # TODO diagnostic
|
||||
# no args/narrowing
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool():
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
|
||||
# invalid invocation, too many positional args
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, 5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
|
||||
# invalid invocation, too many kwargs
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, y=5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
@@ -9,85 +9,67 @@ Similarly, in `and` expressions, the right-hand side is evaluated only if the le
|
||||
## Narrowing in `or`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag else None
|
||||
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
x is None or reveal_type(x) # revealed: A
|
||||
reveal_type(x) # revealed: A | None
|
||||
isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
x is None or reveal_type(x) # revealed: A
|
||||
reveal_type(x) # revealed: A | None
|
||||
```
|
||||
|
||||
## Narrowing in `and`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag else None
|
||||
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
x is None and reveal_type(x) # revealed: None
|
||||
reveal_type(x) # revealed: A | None
|
||||
isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
x is None and reveal_type(x) # revealed: None
|
||||
reveal_type(x) # revealed: A | None
|
||||
```
|
||||
|
||||
## Multiple `and` arms
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag1 else None
|
||||
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
bool_instance() and isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
isinstance(x, A) and bool_instance() and reveal_type(x) # revealed: A
|
||||
reveal_type(x) and isinstance(x, A) and bool_instance() # revealed: A | None
|
||||
flag2 and isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
isinstance(x, A) and flag2 and reveal_type(x) # revealed: A
|
||||
reveal_type(x) and isinstance(x, A) and flag3 # revealed: A | None
|
||||
```
|
||||
|
||||
## Multiple `or` arms
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag1 else None
|
||||
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
bool_instance() or isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
isinstance(x, A) or bool_instance() or reveal_type(x) # revealed: None
|
||||
reveal_type(x) or isinstance(x, A) or bool_instance() # revealed: A | None
|
||||
flag2 or isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
isinstance(x, A) or flag3 or reveal_type(x) # revealed: None
|
||||
reveal_type(x) or isinstance(x, A) or flag4 # revealed: A | None
|
||||
```
|
||||
|
||||
## Multiple predicates
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
|
||||
|
||||
class A: ...
|
||||
|
||||
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
|
||||
|
||||
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
|
||||
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Mix of `and` and `or`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
|
||||
|
||||
class A: ...
|
||||
|
||||
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
|
||||
|
||||
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
|
||||
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -6,15 +6,11 @@
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
def _(x: A | B):
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
```
|
||||
|
||||
## Arms might not add narrowing constraints
|
||||
@@ -23,25 +19,18 @@ else:
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, x: A | B):
|
||||
if isinstance(x, A) and flag:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
if flag and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and bool_instance():
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if bool_instance() and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## Statically known arms
|
||||
@@ -50,39 +39,35 @@ reveal_type(x) # revealed: A | B
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
def _(x: A | B):
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
x = instance()
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## The type of multiple symbols can be narrowed down
|
||||
@@ -91,22 +76,17 @@ reveal_type(x) # revealed: A | B
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
def _(x: A | B, y: A | B):
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
```
|
||||
|
||||
## Narrowing in `or` conditional
|
||||
@@ -116,15 +96,11 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## In `or`, all arms should add constraint in order to narrow
|
||||
@@ -134,18 +110,11 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B) or bool_instance():
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
def _(flag: bool, x: A | B | C):
|
||||
if isinstance(x, A) or isinstance(x, B) or flag:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## in `or`, all arms should narrow the same set of symbols
|
||||
@@ -155,28 +124,23 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
def _(x: A | B | C, y: A | B | C):
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
```
|
||||
|
||||
## mixing `and` and `not`
|
||||
@@ -186,16 +150,12 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
```
|
||||
|
||||
## mixing `or` and `not`
|
||||
@@ -205,15 +165,11 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
```
|
||||
|
||||
## `or` with nested `and`
|
||||
@@ -223,16 +179,12 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
```
|
||||
|
||||
## `and` with nested `or`
|
||||
@@ -242,41 +194,32 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
```
|
||||
|
||||
## Boolean expression internal narrowing
|
||||
|
||||
```py
|
||||
def optional_string() -> str | None:
|
||||
return None
|
||||
def _(x: str | None, y: str | None):
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
|
||||
x = optional_string()
|
||||
y = optional_string()
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
```
|
||||
|
||||
@@ -3,55 +3,47 @@
|
||||
## Positive contributions become negative in elif-else blocks
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
x = int_instance()
|
||||
|
||||
if x == 1:
|
||||
# cannot narrow; could be a subclass of `int`
|
||||
reveal_type(x) # revealed: int
|
||||
elif x == 2:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
elif x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
def _(x: int):
|
||||
if x == 1:
|
||||
# cannot narrow; could be a subclass of `int`
|
||||
reveal_type(x) # revealed: int
|
||||
elif x == 2:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
elif x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
```
|
||||
|
||||
## Positive contributions become negative in elif-else blocks, with simplification
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||
|
||||
if x == 1:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
elif x == 2:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
if x == 1:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
elif x == 2:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## Multiple negative contributions using elif, with simplification
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x != 2:
|
||||
# TODO should be `Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
elif x == 3:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x != 2:
|
||||
# TODO should be `Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
elif x == 3:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
@@ -3,77 +3,64 @@
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
flag = bool_instance()
|
||||
if y is x:
|
||||
reveal_type(y) # revealed: A
|
||||
else:
|
||||
reveal_type(y) # revealed: A | None
|
||||
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
if y is x:
|
||||
reveal_type(y) # revealed: A
|
||||
else:
|
||||
reveal_type(y) # revealed: A | None
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
|
||||
## `is` in chained comparisons
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(x_flag: bool, y_flag: bool):
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is not x) or (x is not False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is not x) or (x is not False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
## `is` in elif clause
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = None if flag1 else (1 if flag2 else True)
|
||||
|
||||
x = None if bool_instance() else (1 if bool_instance() else True)
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
elif x is True:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
elif x is True:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -5,34 +5,28 @@
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
if x is not False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
if x is not False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for non-singleton types
|
||||
@@ -53,20 +47,17 @@ else:
|
||||
## `is not` for other types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
class A: ...
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: A | None
|
||||
else:
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
x = A()
|
||||
y = x if bool_instance() else None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: A | None
|
||||
else:
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
|
||||
## `is not` in chained comparisons
|
||||
@@ -74,23 +65,20 @@ reveal_type(y) # revealed: A | None
|
||||
The type guard removes `False` from the union type of the tested value only.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is x) or (x is False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
def _(x_flag: bool, y_flag: bool):
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is x) or (x is False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -3,54 +3,45 @@
|
||||
## Multiple negative contributions
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
x = int_instance()
|
||||
|
||||
if x != 1:
|
||||
if x != 2:
|
||||
if x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
def _(x: int):
|
||||
if x != 1:
|
||||
if x != 2:
|
||||
if x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
```
|
||||
|
||||
## Multiple negative contributions with simplification
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
flag1, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x != 2:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x != 2:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## elif-else blocks
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x == 2:
|
||||
# TODO should be `Literal[2]`
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x == 3:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
if x == 2:
|
||||
# TODO should be `Literal[2]`
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x == 3:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
elif x != 2:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
elif x != 2:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
@@ -5,29 +5,25 @@ The `not` operator negates a constraint.
|
||||
## `not is None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
x = None if bool_instance() else 1
|
||||
if not x is None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
if not x is None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `not isinstance`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
x = 1 if bool_instance() else "a"
|
||||
|
||||
if not isinstance(x, (int)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if not isinstance(x, (int)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -3,82 +3,66 @@
|
||||
## `x != None`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x != None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
# TODO should be None
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
if x != None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
# TODO should be None
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `!=` for other singleton types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = True if flag else False
|
||||
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
|
||||
if x != False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
# TODO should be Literal[False]
|
||||
reveal_type(x) # revealed: bool
|
||||
if x != False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
# TODO should be Literal[False]
|
||||
reveal_type(x) # revealed: bool
|
||||
```
|
||||
|
||||
## `x != y` where `y` is of literal type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else 2
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## `x != y` where `y` is a single-valued type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
class B: ...
|
||||
C = A if flag else B
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
C = A if flag else B
|
||||
|
||||
if C != A:
|
||||
reveal_type(C) # revealed: Literal[B]
|
||||
else:
|
||||
# TODO should be Literal[A]
|
||||
reveal_type(C) # revealed: Literal[A, B]
|
||||
if C != A:
|
||||
reveal_type(C) # revealed: Literal[B]
|
||||
else:
|
||||
# TODO should be Literal[A]
|
||||
reveal_type(C) # revealed: Literal[A, B]
|
||||
```
|
||||
|
||||
## `x != y` where `y` has multiple single-valued options
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2
|
||||
y = 2 if flag2 else 3
|
||||
|
||||
x = 1 if bool_instance() else 2
|
||||
y = 2 if bool_instance() else 3
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## `!=` for non-single-valued types
|
||||
@@ -86,34 +70,22 @@ else:
|
||||
Only single-valued types should narrow the type:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, a: int, y: int):
|
||||
x = a if flag else None
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
flag = bool_instance()
|
||||
x = int_instance() if flag else None
|
||||
y = int_instance()
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: int | None
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Mix of single-valued and non-single-valued types
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
def _(flag1: bool, flag2: bool, a: int):
|
||||
x = 1 if flag1 else 2
|
||||
y = 2 if flag2 else a
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = 1 if bool_instance() else 2
|
||||
y = 2 if bool_instance() else int_instance()
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
@@ -5,23 +5,19 @@ Narrowing for `isinstance(object, classinfo)` expressions.
|
||||
## `classinfo` is a single type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if isinstance(x, str):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
if isinstance(x, str):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## `classinfo` is a tuple of types
|
||||
@@ -30,56 +26,48 @@ Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tup
|
||||
The former is equivalent to `isinstance(x, int | str)`:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance(x, (int, bytes)):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
if isinstance(x, (bytes, str)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
|
||||
if isinstance(x, (int, bytes)):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
# No narrowing should occur if a larger type is also
|
||||
# one of the possibilities:
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, (bytes, str)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
# No narrowing should occur if a larger type is also
|
||||
# one of the possibilities:
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
```
|
||||
|
||||
## `classinfo` is a nested tuple of types
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (bool, (bytes, int))):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, (bool, (bytes, int))):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
## Class types
|
||||
@@ -89,9 +77,7 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
x = object()
|
||||
|
||||
if isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
@@ -112,50 +98,40 @@ else:
|
||||
## No narrowing for instances of `builtins.type`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
t = type("t", (), {})
|
||||
|
||||
flag = bool_instance()
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
|
||||
t = type("t", (), {})
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
```
|
||||
|
||||
## Do not use custom `isinstance` for narrowing
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
def isinstance(x, t):
|
||||
return True
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
def isinstance(x, t):
|
||||
return True
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is aliased
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
isinstance_alias = isinstance
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
isinstance_alias = isinstance
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance_alias(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if isinstance_alias(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is imported
|
||||
@@ -163,46 +139,38 @@ if isinstance_alias(x, int):
|
||||
```py
|
||||
from builtins import isinstance as imported_isinstance
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
if imported_isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if imported_isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Do not narrow if second argument is not a type
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "int"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "int"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`isinstance` has no `foo` parameter)
|
||||
if isinstance(x, int, foo="bar"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`isinstance` has no `foo` parameter)
|
||||
if isinstance(x, int, foo="bar"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
@@ -7,45 +7,43 @@ Narrowing for `issubclass(class, classinfo)` expressions.
|
||||
### Basic example
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
t = int if flag else str
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Proper narrowing in `elif` and `else` branches
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
def _(flag1: bool, flag2: bool):
|
||||
t = int if flag1 else str if flag2 else bytes
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
@@ -56,29 +54,28 @@ class Derived1(Base): ...
|
||||
class Derived2(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
def _(flag1: bool, flag2: bool, flag3: bool):
|
||||
t1 = Derived1 if flag1 else Derived2
|
||||
|
||||
t1 = Derived1 if flag() else Derived2
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
t2 = Derived1 if flag2 else Base
|
||||
|
||||
t2 = Derived1 if flag() else Base
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
t3 = Derived1 if flag3 else Unrelated
|
||||
|
||||
t3 = Derived1 if flag() else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
@@ -87,16 +84,13 @@ else:
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_class() -> type[object]: ...
|
||||
|
||||
t = get_class()
|
||||
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
def _(t: type[object]):
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
@@ -107,16 +101,15 @@ else:
|
||||
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
|
||||
from types import NoneType
|
||||
|
||||
def flag() -> bool: ...
|
||||
def _(flag: bool):
|
||||
t = int if flag else NoneType
|
||||
|
||||
t = int if flag() else NoneType
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
@@ -126,14 +119,13 @@ if issubclass(t, type(None)):
|
||||
```py
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
def _(flag1: bool, flag2: bool):
|
||||
t = int if flag1 else str if flag2 else bytes
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Special cases
|
||||
@@ -148,9 +140,7 @@ to `issubclass`:
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
t = get_object()
|
||||
t = object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, A):
|
||||
|
||||
@@ -3,19 +3,16 @@
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
flag = bool_instance()
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
y = 0
|
||||
|
||||
y = 0
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
```
|
||||
|
||||
@@ -3,62 +3,49 @@
|
||||
## After if-else statements, narrowing has no effect if the variable is not mutated in any branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
def _(x: int | None):
|
||||
if x is None:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int | None
|
||||
reveal_type(x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Narrowing can have a persistent effect if the variable is mutated in one branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
def _(x: int | None):
|
||||
if x is None:
|
||||
x = 10
|
||||
else:
|
||||
pass
|
||||
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 10
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## An if statement without an explicit `else` branch is equivalent to one with a no-op `else` branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
def _(x: int | None, y: int | None):
|
||||
if x is None:
|
||||
x = 0
|
||||
|
||||
x = optional_int()
|
||||
y = optional_int()
|
||||
if y is None:
|
||||
pass
|
||||
|
||||
if x is None:
|
||||
x = 0
|
||||
|
||||
if y is None:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: int | None
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: int | None
|
||||
```
|
||||
|
||||
## An if-elif without an explicit else branch is equivalent to one with an empty else branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
def _(x: int | None):
|
||||
if x is None:
|
||||
x = 0
|
||||
elif x > 50:
|
||||
x = 50
|
||||
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 0
|
||||
elif x > 50:
|
||||
x = 50
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Narrowing For Truthiness Checks (`if x` or `if not x`)
|
||||
|
||||
## Value Literals
|
||||
|
||||
```py
|
||||
def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]:
|
||||
return 0
|
||||
|
||||
x = foo()
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
|
||||
if not x:
|
||||
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
|
||||
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
|
||||
if not (x and not x):
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if not (x or not x):
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
|
||||
|
||||
if (isinstance(x, int) or isinstance(x, str)) and x:
|
||||
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
|
||||
```
|
||||
|
||||
## Function Literals
|
||||
|
||||
Basically functions are always truthy.
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
def foo(hello: int) -> bytes:
|
||||
return b""
|
||||
|
||||
def bar(world: str, *args, **kwargs) -> float:
|
||||
return 0.0
|
||||
|
||||
x = foo if flag() else bar
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[foo, bar]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
## Mutable Truthiness
|
||||
|
||||
### Truthiness of Instances
|
||||
|
||||
The boolean value of an instance is not always consistent. For example, `__bool__` can be customized
|
||||
to return random values, or in the case of a `list()`, the result depends on the number of elements
|
||||
in the list. Therefore, these types should not be narrowed by `if x` or `if not x`.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def f(x: A | B):
|
||||
if x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
|
||||
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy | A & ~AlwaysFalsy | B & ~AlwaysFalsy
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy | A & ~AlwaysTruthy | B & ~AlwaysTruthy
|
||||
else:
|
||||
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
|
||||
```
|
||||
|
||||
### Truthiness of Types
|
||||
|
||||
Also, types may not be Truthy. This is because `__bool__` can be customized via a metaclass.
|
||||
Although this is a very rare case, we may consider metaclass checks in the future to handle this
|
||||
more accurately.
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
x = int if flag() else str
|
||||
reveal_type(x) # revealed: Literal[int, str]
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
|
||||
```
|
||||
|
||||
## Determined Truthiness
|
||||
|
||||
Some custom classes can have a boolean value that is consistently determined as either `True` or
|
||||
`False`, regardless of the instance's state. This is achieved by defining a `__bool__` method that
|
||||
always returns a fixed value.
|
||||
|
||||
These types can always be fully narrowed in boolean contexts, as shown below:
|
||||
|
||||
```py
|
||||
class T:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class F:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
t = T()
|
||||
|
||||
if t:
|
||||
reveal_type(t) # revealed: T
|
||||
else:
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
f = F()
|
||||
|
||||
if f:
|
||||
reveal_type(f) # revealed: Never
|
||||
else:
|
||||
reveal_type(f) # revealed: F
|
||||
```
|
||||
|
||||
## Narrowing Complex Intersection and Union
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
def literals() -> Literal[0, 42, "", "hello"]:
|
||||
return 42
|
||||
|
||||
x = instance()
|
||||
y = literals()
|
||||
|
||||
if isinstance(x, str) and not isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & str & ~B
|
||||
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
|
||||
|
||||
z = x if flag() else y
|
||||
|
||||
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
|
||||
|
||||
if z:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
|
||||
else:
|
||||
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
|
||||
```
|
||||
|
||||
## Narrowing Multiple Variables
|
||||
|
||||
```py
|
||||
def f(x: Literal[0, 1], y: Literal["", "hello"]):
|
||||
if x and y and not x and not y:
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(y) # revealed: Never
|
||||
else:
|
||||
# ~(x or not x) and ~(y or not y)
|
||||
reveal_type(x) # revealed: Literal[0, 1]
|
||||
reveal_type(y) # revealed: Literal["", "hello"]
|
||||
|
||||
if (x or not x) and (y and not y):
|
||||
reveal_type(x) # revealed: Literal[0, 1]
|
||||
reveal_type(y) # revealed: Never
|
||||
else:
|
||||
# ~(x or not x) or ~(y and not y)
|
||||
reveal_type(x) # revealed: Literal[0, 1]
|
||||
reveal_type(y) # revealed: Literal["", "hello"]
|
||||
```
|
||||
|
||||
## ControlFlow Merging
|
||||
|
||||
After merging control flows, when we take the union of all constraints applied in each branch, we
|
||||
should return to the original state.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
|
||||
if x and not x:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
|
||||
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
|
||||
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
|
||||
```
|
||||
@@ -6,18 +6,14 @@
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
# It would be wrong to infer `B` here. The type
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def _(x: A | B):
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
# It would be wrong to infer `B` here. The type
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## `type(x) is not C`
|
||||
@@ -26,16 +22,12 @@ else:
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
def _(x: A | B):
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## `type(x) == C`, `type(x) != C`
|
||||
@@ -54,16 +46,12 @@ class IsEqualToEverything(type):
|
||||
class A(metaclass=IsEqualToEverything): ...
|
||||
class B(metaclass=IsEqualToEverything): ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return B()
|
||||
def _(x: A | B):
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for custom `type` callable
|
||||
@@ -75,15 +63,11 @@ class B: ...
|
||||
def type(x):
|
||||
return int
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def _(x: A | B):
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for multiple arguments
|
||||
@@ -91,15 +75,11 @@ else:
|
||||
No narrowing should occur if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
def _(x: str | int):
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## No narrowing for keyword arguments
|
||||
@@ -107,14 +87,10 @@ else:
|
||||
`type` can't be used with a keyword argument:
|
||||
|
||||
```py
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
# TODO: we could issue a diagnostic here
|
||||
if type(object=x) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
def _(x: str | int):
|
||||
# TODO: we could issue a diagnostic here
|
||||
if type(object=x) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## Narrowing if `type` is aliased
|
||||
@@ -125,13 +101,9 @@ class B: ...
|
||||
|
||||
alias_for_type = type
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
def _(x: A | B):
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## Limitations
|
||||
@@ -140,13 +112,9 @@ if alias_for_type(x) is A:
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
|
||||
def get_base() -> Base:
|
||||
return Base()
|
||||
|
||||
x = get_base()
|
||||
|
||||
if type(x) is Base:
|
||||
# Ideally, this could be narrower, but there is now way to
|
||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||
reveal_type(x) # revealed: Base
|
||||
def _(x: Base):
|
||||
if type(x) is Base:
|
||||
# Ideally, this could be narrower, but there is now way to
|
||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||
reveal_type(x) # revealed: Base
|
||||
```
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Narrowing in `while` loops
|
||||
|
||||
We only make sure that narrowing works for `while` loops in general, we do not exhaustively test all
|
||||
narrowing forms here, as they are covered in other tests.
|
||||
|
||||
Note how type narrowing works subtly different from `if` ... `else`, because the negated constraint
|
||||
is retained after the loop.
|
||||
|
||||
## Basic `while` loop
|
||||
|
||||
```py
|
||||
def next_item() -> int | None: ...
|
||||
|
||||
x = next_item()
|
||||
|
||||
while x is not None:
|
||||
reveal_type(x) # revealed: int
|
||||
x = next_item()
|
||||
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
## `while` loop with `else`
|
||||
|
||||
```py
|
||||
def next_item() -> int | None: ...
|
||||
|
||||
x = next_item()
|
||||
|
||||
while x is not None:
|
||||
reveal_type(x) # revealed: int
|
||||
x = next_item()
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def next_item() -> Literal[1, 2, 3]: ...
|
||||
|
||||
x = next_item()
|
||||
|
||||
while x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
|
||||
while x != 2:
|
||||
# TODO: this should be Literal[1, 3]; Literal[3] is only correct
|
||||
# in the first loop iteration
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
x = next_item()
|
||||
|
||||
x = next_item()
|
||||
```
|
||||
@@ -1,4 +1,11 @@
|
||||
# Type aliases
|
||||
# PEP 695 type aliases
|
||||
|
||||
PEP 695 type aliases are only available in Python 3.12 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
## Basic
|
||||
|
||||
@@ -10,10 +10,10 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
copyright = 1
|
||||
chr = 1
|
||||
|
||||
def f():
|
||||
reveal_type(copyright) # revealed: Literal[copyright] | Literal[1]
|
||||
reveal_type(chr) # revealed: Literal[chr] | Literal[1]
|
||||
```
|
||||
|
||||
## Conditionally global or builtin, with annotation
|
||||
@@ -25,8 +25,8 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
copyright: int = 1
|
||||
chr: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(copyright) # revealed: Literal[copyright] | int
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
flag = coinflip()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
@@ -24,14 +24,14 @@ reveal_type(C.y) # revealed: Literal[1]
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
if coinflip():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
if coinflip():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
## Shadow after incompatible declarations is OK
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x: bytes = b"foo"
|
||||
x: bytes = b"foo"
|
||||
```
|
||||
|
||||
@@ -22,12 +22,10 @@ reveal_type(x) # revealed: Unknown
|
||||
y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
|
||||
reveal_type(y) # revealed: Unknown
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
a = b"abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
def _(n: int):
|
||||
a = b"abcde"[n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -43,15 +41,13 @@ b[:4:0] # error: [zero-stepsize-in-slice]
|
||||
b[0::0] # error: [zero-stepsize-in-slice]
|
||||
b[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def int_instance() -> int: ...
|
||||
def _(m: int, n: int):
|
||||
byte_slice1 = b[m:n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
|
||||
byte_slice1 = b[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
|
||||
def bytes_instance() -> bytes: ...
|
||||
|
||||
byte_slice2 = bytes_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
def _(s: bytes) -> bytes:
|
||||
byte_slice2 = s[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -21,77 +21,66 @@ reveal_type(Identity[0]) # revealed: str
|
||||
## Class getitem union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class UnionClassGetItem:
|
||||
if flag:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
else:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
class UnionClassGetItem:
|
||||
if bool_instance():
|
||||
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
else:
|
||||
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with class union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
x = A if flag else B
|
||||
|
||||
x = A if bool_instance() else B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with unbound method union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class Spam:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
if bool_instance():
|
||||
class Spam:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
else:
|
||||
class Spam: ...
|
||||
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
else:
|
||||
class Spam: ...
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
```
|
||||
|
||||
## TODO: Class getitem non-class union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class Eggs:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
if bool_instance():
|
||||
class Eggs:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
else:
|
||||
Eggs = 1
|
||||
|
||||
else:
|
||||
Eggs = 1
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
|
||||
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
|
||||
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -30,18 +30,14 @@ reveal_type(Identity()[0]) # revealed: int
|
||||
## Getitem union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
def _(flag: bool):
|
||||
class Identity:
|
||||
if flag:
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
else:
|
||||
def __getitem__(self, index: int) -> str:
|
||||
return str(index)
|
||||
|
||||
class Identity:
|
||||
if bool_instance():
|
||||
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
else:
|
||||
|
||||
def __getitem__(self, index: int) -> str:
|
||||
return str(index)
|
||||
|
||||
reveal_type(Identity()[0]) # revealed: int | str
|
||||
reveal_type(Identity()[0]) # revealed: int | str
|
||||
```
|
||||
|
||||
@@ -19,11 +19,10 @@ reveal_type(a) # revealed: Unknown
|
||||
b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
def int_instance() -> int: ...
|
||||
|
||||
a = "abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
def _(n: int):
|
||||
a = "abcde"[n]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -74,17 +73,14 @@ s[:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[0::0] # error: [zero-stepsize-in-slice]
|
||||
s[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def int_instance() -> int: ...
|
||||
def _(m: int, n: int, s2: str):
|
||||
substring1 = s[m:n]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
|
||||
substring1 = s[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
|
||||
def str_instance() -> str: ...
|
||||
|
||||
substring2 = str_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
substring2 = s2[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
@@ -67,9 +67,60 @@ t[:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[0::0] # error: [zero-stepsize-in-slice]
|
||||
t[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def int_instance() -> int: ...
|
||||
|
||||
tuple_slice = t[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
def _(m: int, n: int):
|
||||
tuple_slice = t[m:n]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO:
|
||||
# * `tuple.__class_getitem__` is always bound on 3.9 (`sys.version_info`)
|
||||
# * `tuple[int, str]` is a valid base (generics)
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
|
||||
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class A(tuple[int, str]): ...
|
||||
|
||||
# Runtime value: `(A, tuple, object)`
|
||||
# TODO: Generics
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `typing.Tuple`
|
||||
|
||||
### Correspondence with `tuple`
|
||||
|
||||
`typing.Tuple` can be used interchangeably with `tuple`:
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]):
|
||||
reveal_type(c) # revealed: tuple
|
||||
reveal_type(d) # revealed: tuple[int, A]
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
### Inheritance
|
||||
|
||||
Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself
|
||||
is not a class.
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
class C(Tuple): ...
|
||||
|
||||
# Runtime value: `(C, tuple, typing.Generic, object)`
|
||||
# TODO: Add `Generic` to the MRO
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[tuple], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Suppressing errors with `type: ignore`
|
||||
|
||||
Type check errors can be suppressed by a `type: ignore` comment on the same line as the violation.
|
||||
|
||||
## Simple `type: ignore`
|
||||
|
||||
```py
|
||||
a = 4 + test # type: ignore
|
||||
```
|
||||
|
||||
## Multiline ranges
|
||||
|
||||
A diagnostic with a multiline range can be suppressed by a comment on the same line as the
|
||||
diagnostic's start or end. This is the same behavior as Mypy's.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
y = (
|
||||
4 / 0 # type: ignore
|
||||
)
|
||||
|
||||
y = (
|
||||
4 / # type: ignore
|
||||
0
|
||||
)
|
||||
|
||||
y = (
|
||||
4 /
|
||||
0 # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
Pyright diverges from this behavior and instead applies a suppression if its range intersects with
|
||||
the diagnostic range. This can be problematic for nested expressions because a suppression in a
|
||||
child expression now suppresses errors in the outer expression.
|
||||
|
||||
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
```py path=nested.py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
y = (
|
||||
cast(int, "test" +
|
||||
2 # type: ignore
|
||||
)
|
||||
+ "other" # TODO: expected-error[invalid-operator]
|
||||
)
|
||||
```
|
||||
|
||||
Mypy flags the second usage.
|
||||
|
||||
## Before opening parenthesis
|
||||
|
||||
A suppression that applies to all errors before the opening parenthesis.
|
||||
|
||||
```py
|
||||
a: Test = ( # type: ignore
|
||||
Test() # error: [unresolved-reference]
|
||||
) # fmt: skip
|
||||
```
|
||||
|
||||
## Multiline string
|
||||
|
||||
```py
|
||||
a: int = 4
|
||||
a = """
|
||||
This is a multiline string and the suppression is at its end
|
||||
""" # type: ignore
|
||||
```
|
||||
|
||||
## Line continuations
|
||||
|
||||
Suppressions after a line continuation apply to all previous lines.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
a = test \
|
||||
+ 2 # type: ignore
|
||||
|
||||
a = test \
|
||||
+ a \
|
||||
+ 2 # type: ignore
|
||||
```
|
||||
|
||||
## Codes
|
||||
|
||||
Mypy supports `type: ignore[code]`. Red Knot doesn't understand mypy's rule names. Therefore, ignore
|
||||
the codes and suppress all errors.
|
||||
|
||||
```py
|
||||
a = test # type: ignore[name-defined]
|
||||
```
|
||||
|
||||
## Nested comments
|
||||
|
||||
TODO: We should support this for better interopability with other suppression comments.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
# TODO this error should be suppressed
|
||||
# error: [unresolved-reference]
|
||||
a = test \
|
||||
+ 2 # fmt: skip # type: ignore
|
||||
|
||||
a = test \
|
||||
+ 2 # type: ignore # fmt: skip
|
||||
```
|
||||
|
||||
## Misspelled `type: ignore`
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
a = test + 2 # type: ignoree
|
||||
```
|
||||
|
||||
## Invalid - ignore on opening parentheses
|
||||
|
||||
`type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses
|
||||
in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot.
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
a = ( # type: ignore
|
||||
test + 4 # error: [unresolved-reference]
|
||||
)
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.9"
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
## The type of `sys.version_info`
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
def _(c: type[A]):
|
||||
reveal_type(c) # revealed: type[A]
|
||||
```
|
||||
|
||||
## Nested class literal
|
||||
@@ -17,10 +15,8 @@ reveal_type(f()) # revealed: type[A]
|
||||
class A:
|
||||
class B: ...
|
||||
|
||||
def f() -> type[A.B]:
|
||||
return A.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
def f(c: type[A.B]):
|
||||
reveal_type(c) # revealed: type[B]
|
||||
```
|
||||
|
||||
## Deeply nested class literal
|
||||
@@ -30,10 +26,8 @@ class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f() -> type[A.B.C]:
|
||||
return A.B.C
|
||||
|
||||
reveal_type(f()) # revealed: type[C]
|
||||
def f(c: type[A.B.C]):
|
||||
reveal_type(c) # revealed: type[C]
|
||||
```
|
||||
|
||||
## Class literal from another module
|
||||
@@ -41,10 +35,8 @@ reveal_type(f()) # revealed: type[C]
|
||||
```py
|
||||
from a import A
|
||||
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
def f(c: type[A]):
|
||||
reveal_type(c) # revealed: type[A]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
@@ -56,10 +48,8 @@ class A: ...
|
||||
```py
|
||||
import a
|
||||
|
||||
def f() -> type[a.B]:
|
||||
return a.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
def f(c: type[a.B]):
|
||||
reveal_type(c) # revealed: type[B]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
@@ -71,14 +61,8 @@ class B: ...
|
||||
```py path=a/test.py
|
||||
import a.b
|
||||
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
def f() -> type[a.b.C]:
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
return a.b.C
|
||||
|
||||
reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
|
||||
def f(c: type[a.b.C]):
|
||||
reveal_type(c) # revealed: type[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
@@ -88,7 +72,7 @@ reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Union of classes
|
||||
## New-style union of classes
|
||||
|
||||
```py
|
||||
class BasicUser: ...
|
||||
@@ -98,11 +82,44 @@ class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def get_user() -> type[BasicUser | ProUser | A.B.C]:
|
||||
return BasicUser
|
||||
def _(u: type[BasicUser | ProUser | A.B.C]):
|
||||
# revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(u)
|
||||
```
|
||||
|
||||
# revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(get_user())
|
||||
## Old-style union of classes
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
class BasicUser: ...
|
||||
class ProUser: ...
|
||||
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]):
|
||||
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(b) # revealed: type[str]
|
||||
reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
```
|
||||
|
||||
## New-style and old-style unions in combination
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
class BasicUser: ...
|
||||
class ProUser: ...
|
||||
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]):
|
||||
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str]
|
||||
```
|
||||
|
||||
## Illegal parameters
|
||||
@@ -112,6 +129,16 @@ class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def get_user() -> type[A, B]:
|
||||
return A
|
||||
_: type[A, B]
|
||||
```
|
||||
|
||||
## As a base class
|
||||
|
||||
```py
|
||||
# TODO: this is a false positive
|
||||
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(type[int]): ...
|
||||
|
||||
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user