Compare commits
1 Commits
0.8.4
...
micha/disp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e02fe815e8 |
9
.github/actionlint.yaml
vendored
9
.github/actionlint.yaml
vendored
@@ -1,9 +0,0 @@
|
||||
# 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
|
||||
8
.github/workflows/build-binaries.yml
vendored
8
.github/workflows/build-binaries.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test sdist"
|
||||
run: |
|
||||
pip install dist/"${PACKAGE_NAME}"-*.tar.gz --force-reinstall
|
||||
pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload sdist"
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - aarch64"
|
||||
run: |
|
||||
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
python -m pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
- name: "Test wheel"
|
||||
if: ${{ startsWith(matrix.target, 'x86_64') }}
|
||||
run: |
|
||||
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
|
||||
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
|
||||
13
.github/workflows/build-docker.yml
vendored
13
.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
|
||||
@@ -142,10 +142,9 @@ 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 "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
$(printf '${RUFF_BASE_IMG}@sha256:%s ' *)
|
||||
|
||||
docker-publish-extra:
|
||||
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
|
||||
@@ -204,14 +203,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
|
||||
@@ -287,9 +286,7 @@ 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 "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
$(printf '${RUFF_BASE_IMG}@sha256:%s ' *)
|
||||
|
||||
72
.github/workflows/ci.yaml
vendored
72
.github/workflows/ci.yaml
vendored
@@ -290,9 +290,7 @@ jobs:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: rustup default "${MSRV}"
|
||||
run: rustup default ${{ steps.msrv.outputs.value }}
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
@@ -308,14 +306,13 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
|
||||
run: cargo +${{ steps.msrv.outputs.value }} 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' || needs.determine_changes.outputs.code == 'true' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -357,18 +354,16 @@ 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 "${DOWNLOAD_PATH}/ruff"
|
||||
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
(
|
||||
uvx \
|
||||
--python="${PYTHON_VERSION}" \
|
||||
--python=${{ env.PYTHON_VERSION }} \
|
||||
--from=./python/py-fuzzer \
|
||||
fuzz \
|
||||
--test-executable="${DOWNLOAD_PATH}/ruff" \
|
||||
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
|
||||
--bin=ruff \
|
||||
0-500
|
||||
)
|
||||
@@ -434,72 +429,64 @@ 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 "${DOWNLOAD_PATH}/ruff"
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.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 "${DOWNLOAD_PATH}/ruff"
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.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 "${DOWNLOAD_PATH}/ruff"
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.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 "${DOWNLOAD_PATH}/ruff"
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.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
|
||||
@@ -554,7 +541,7 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
run: |
|
||||
pip install --force-reinstall --find-links dist "${PACKAGE_NAME}"
|
||||
pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }}
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Remove wheels from cache"
|
||||
@@ -583,14 +570,13 @@ 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
|
||||
# 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"
|
||||
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
|
||||
|
||||
docs:
|
||||
name: "mkdocs"
|
||||
@@ -651,7 +637,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
|
||||
|
||||
@@ -690,13 +676,11 @@ 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 "${DOWNLOAD_PATH}/ruff"
|
||||
export PATH="${DOWNLOAD_PATH}:${PATH}"
|
||||
chmod +x ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
export PATH=${{ steps.ruff-target.outputs.download-path }}:$PATH
|
||||
ruff version
|
||||
|
||||
just test
|
||||
|
||||
1
.github/workflows/daily_fuzz.yaml
vendored
1
.github/workflows/daily_fuzz.yaml
vendored
@@ -46,7 +46,6 @@ 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,11 +10,12 @@ 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
|
||||
@@ -29,7 +30,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
|
||||
@@ -65,9 +66,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
|
||||
|
||||
20
.github/workflows/publish-docs.yml
vendored
20
.github/workflows/publish-docs.yml
vendored
@@ -44,8 +44,8 @@ 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: |
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
# characters disallowed in git branch names with hyphens
|
||||
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' }}
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
# 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
|
||||
@@ -124,12 +124,10 @@ jobs:
|
||||
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 }}
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -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
6
.github/zizmor.yml
vendored
@@ -1,6 +0,0 @@
|
||||
# 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,11 +21,3 @@ 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,7 +2,6 @@ fail_fast: false
|
||||
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.github/workflows/release.yml|
|
||||
crates/red_knot_vendored/vendor/.*|
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
@@ -27,8 +26,9 @@ repos:
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs==4.0.0
|
||||
- mdformat-footnote==0.1.1
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
- mdformat-footnote
|
||||
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.3
|
||||
rev: v1.28.2
|
||||
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.3
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -88,37 +88,18 @@ 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.9.2
|
||||
rev: v0.8.0
|
||||
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
|
||||
|
||||
- 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,68 +1,5 @@
|
||||
# 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
|
||||
|
||||
72
Cargo.lock
generated
72
Cargo.lock
generated
@@ -220,9 +220,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.11.1"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
|
||||
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
@@ -314,9 +314,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.39"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -465,12 +465,12 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.2.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -923,9 +923,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
version = "0.7.1"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||
checksum = "69ff9c9d5fb3e6da8ac2f77ab76fe7e8087d512ce095200f8f29ac5b656cf6dc"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
@@ -1521,9 +1521,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.168"
|
||||
version = "0.2.167"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
|
||||
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -2161,7 +2161,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2288,7 +2288,6 @@ dependencies = [
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
"drop_bomb",
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"insta",
|
||||
@@ -2315,7 +2314,7 @@ dependencies = [
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -2412,7 +2411,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa",
|
||||
"serde",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
@@ -2518,7 +2517,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.4"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2565,7 +2564,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -2635,7 +2634,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -2737,7 +2736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.4"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2787,7 +2786,7 @@ dependencies = [
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"toml",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -2821,7 +2820,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2893,7 +2892,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3026,7 +3025,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.7",
|
||||
"thiserror 2.0.6",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -3052,7 +3051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.4"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3194,7 +3193,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -3204,7 +3203,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.0",
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
@@ -3215,12 +3213,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=254c749b02cde2fd29852a7463a33e800b771758#254c749b02cde2fd29852a7463a33e800b771758"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3282,9 +3280,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.216"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3302,9 +3300,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.216"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3625,11 +3623,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.7"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
|
||||
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.7",
|
||||
"thiserror-impl 2.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3645,9 +3643,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.7"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
|
||||
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -118,8 +118,7 @@ rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# 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" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
|
||||
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.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.4/install.ps1 | iex"
|
||||
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"
|
||||
```
|
||||
|
||||
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.4
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -16,11 +15,12 @@ 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 python_version;
|
||||
mod target_version;
|
||||
mod verbosity;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -34,39 +34,54 @@ struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Option<Command>,
|
||||
|
||||
/// 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 = "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>,
|
||||
|
||||
/// 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")]
|
||||
#[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"
|
||||
)]
|
||||
venv_path: 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 = "DIRECTORY",
|
||||
help = "Custom directory to use for stdlib typeshed stubs"
|
||||
)]
|
||||
custom_typeshed_dir: Option<SystemPathBuf>,
|
||||
|
||||
/// Additional path to use as a module-resolution source (can be passed multiple times).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PATH",
|
||||
help = "Additional path to use as a module-resolution source (can be passed multiple times)"
|
||||
)]
|
||||
extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
/// Python version to assume when resolving types.
|
||||
#[arg(long, value_name = "VERSION", alias = "target-version")]
|
||||
python_version: Option<PythonVersion>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Python version to assume when resolving types",
|
||||
value_name = "VERSION"
|
||||
)]
|
||||
target_version: Option<TargetVersion>,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity,
|
||||
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(long, short = 'W')]
|
||||
#[arg(
|
||||
long,
|
||||
help = "Run in watch mode by re-running whenever files change",
|
||||
short = 'W'
|
||||
)]
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
@@ -74,8 +89,8 @@ impl Args {
|
||||
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
|
||||
let mut configuration = Configuration::default();
|
||||
|
||||
if let Some(python_version) = self.python_version {
|
||||
configuration.python_version = Some(python_version.into());
|
||||
if let Some(target_version) = self.target_version {
|
||||
configuration.target_version = Some(target_version.into());
|
||||
}
|
||||
|
||||
if let Some(venv_path) = &self.venv_path {
|
||||
@@ -84,8 +99,9 @@ impl Args {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(typeshed) = &self.typeshed {
|
||||
configuration.search_paths.typeshed = Some(SystemPath::absolute(typeshed, cli_cwd));
|
||||
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(extra_search_paths) = &self.extra_search_path {
|
||||
@@ -151,13 +167,15 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
};
|
||||
|
||||
let cwd = args
|
||||
.project
|
||||
.current_directory
|
||||
.as_ref()
|
||||
.map(|cwd| {
|
||||
if cwd.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(cwd, &cli_base_path))
|
||||
} else {
|
||||
Err(anyhow!("Provided project path `{cwd}` is not a directory"))
|
||||
Err(anyhow!(
|
||||
"Provided current-directory path `{cwd}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
@@ -279,7 +297,7 @@ impl MainLoop {
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
MainLoopMessage::CheckWorkspace => {
|
||||
let db = db.clone();
|
||||
let db = db.snapshot();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the workspace. This needs to be done in a separate thread
|
||||
|
||||
@@ -1,68 +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 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
62
crates/red_knot/src/target_version.rs
Normal file
62
crates/red_knot/src/target_version.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
/// 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.typeshed.iter())
|
||||
.chain(search_paths.custom_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 {
|
||||
python_version: Some(PythonVersion::PY312),
|
||||
target_version: Some(PythonVersion::PY312),
|
||||
search_paths,
|
||||
};
|
||||
|
||||
@@ -888,7 +888,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
},
|
||||
|root_path, _workspace_path| SearchPathConfiguration {
|
||||
typeshed: Some(root_path.join("typeshed")),
|
||||
custom_typeshed: Some(root_path.join("typeshed")),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -26,7 +26,6 @@ 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 }
|
||||
@@ -54,8 +53,5 @@ 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
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# `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]]
|
||||
```
|
||||
@@ -77,7 +77,7 @@ def _(s: Subclass):
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
|
||||
# error: [invalid-type-parameter] "Type `typing.Any` expected no type parameter"
|
||||
def f(x: Any[int]):
|
||||
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-type-form]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
|
||||
```
|
||||
|
||||
### Parameterized
|
||||
### Parametrized
|
||||
|
||||
`LiteralString` cannot be parameterized.
|
||||
`LiteralString` cannot be parametrized.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
a: LiteralString[str] # error: [invalid-type-form]
|
||||
b: LiteralString["foo"] # error: [invalid-type-form]
|
||||
a: LiteralString[str] # error: [invalid-type-parameter]
|
||||
b: LiteralString["foo"] # error: [invalid-type-parameter]
|
||||
```
|
||||
|
||||
### 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(Attribute access on `StringLiteral` types)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(call todo)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(call todo)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
@@ -135,7 +135,7 @@ if "" < lorem == "ipsum":
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -21,7 +21,7 @@ reveal_type(stop())
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Never` expected no type parameter"
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
a2: Never
|
||||
@@ -51,7 +51,7 @@ def f():
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
# 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__)
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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]
|
||||
```
|
||||
@@ -67,6 +67,6 @@ def _(flag: bool):
|
||||
def __call__(self) -> 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
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
@@ -3,43 +3,40 @@
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Without wildcard
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
match 0:
|
||||
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
|
||||
def _(target: int):
|
||||
y = 1
|
||||
y = 2
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
match target:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
match 0:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
@@ -19,17 +19,14 @@ def _(flag: bool):
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations for 2 (out of 3) types
|
||||
## Partial declarations
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1:
|
||||
x: str
|
||||
elif flag2:
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: int
|
||||
|
||||
# Here, the declared type for `x` is `int | str | Unknown`.
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations with bad assignment
|
||||
@@ -45,31 +42,3 @@ def _(flag: bool):
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -90,83 +90,3 @@ def foo(
|
||||
# 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,12 +1,5 @@
|
||||
# `except*`
|
||||
|
||||
`except*` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
## `except*` with `BaseException`
|
||||
|
||||
```py
|
||||
|
||||
@@ -25,82 +25,3 @@ 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.chr
|
||||
reveal_type(x) # revealed: Literal[chr]
|
||||
x = builtins.copyright
|
||||
reveal_type(x) # revealed: Literal[copyright]
|
||||
```
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# 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,25 +7,3 @@ 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,44 +121,23 @@ X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import foo
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(foo.X) # revealed: Literal[42]
|
||||
y = foo.X
|
||||
|
||||
# TODO: should be `Literal[42]`
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,52 +0,0 @@
|
||||
# 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]
|
||||
```
|
||||
@@ -45,19 +45,19 @@ def f():
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid3: Literal[(3, 4)]
|
||||
|
||||
hello = "hello"
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-type-form]
|
||||
1 + 2, # error: [invalid-literal-parameter]
|
||||
"foo",
|
||||
hello, # error: [invalid-type-form]
|
||||
(1, 2, 3), # error: [invalid-type-form]
|
||||
hello, # error: [invalid-literal-parameter]
|
||||
(1, 2, 3), # error: [invalid-literal-parameter]
|
||||
]
|
||||
```
|
||||
|
||||
@@ -91,13 +91,3 @@ 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
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# While loops
|
||||
|
||||
## Basic `while` loop
|
||||
## Basic While Loop
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
@@ -11,7 +11,7 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## `while` with `else` (no `break`)
|
||||
## While with else (no break)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
@@ -25,7 +25,7 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## `while` with `else` (may `break`)
|
||||
## While with Else (may break)
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
@@ -44,7 +44,7 @@ def _(flag: bool, flag2: bool):
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
## Nested while loops
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
@@ -69,50 +69,3 @@ 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]
|
||||
python-version = "3.10"
|
||||
target-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]
|
||||
python-version = "3.11"
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
@@ -55,7 +55,7 @@ Children in this section should all use the section configuration:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
target-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: type[Unknown]
|
||||
reveal_type(C.__class__) # revealed: 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: type[Unknown]
|
||||
reveal_type(B.__class__) # revealed: 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: type[Unknown]
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown
|
||||
@@ -183,7 +183,7 @@ 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: type[Unknown]
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
@@ -194,26 +194,3 @@ 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__)
|
||||
```
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,58 +0,0 @@
|
||||
# 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()
|
||||
```
|
||||
@@ -10,10 +10,10 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
chr = 1
|
||||
copyright = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | Literal[1]
|
||||
reveal_type(copyright) # revealed: Literal[copyright] | Literal[1]
|
||||
```
|
||||
|
||||
## Conditionally global or builtin, with annotation
|
||||
@@ -25,8 +25,8 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
chr: int = 1
|
||||
copyright: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
reveal_type(copyright) # revealed: Literal[copyright] | int
|
||||
```
|
||||
|
||||
@@ -77,7 +77,7 @@ def _(m: int, n: int):
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
target-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
target-version = "3.9"
|
||||
```
|
||||
|
||||
## The type of `sys.version_info`
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
# PEP 695 type aliases
|
||||
|
||||
PEP 695 type aliases are only available in Python 3.12 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
# Type aliases
|
||||
|
||||
## Basic
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# type[Any]
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f(x: type[Any]):
|
||||
reveal_type(x) # revealed: type[Any]
|
||||
# TODO: could be `<object.__repr__ type> & Any`
|
||||
reveal_type(x.__repr__) # revealed: Any
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type[Any] = object
|
||||
x: type[Any] = type
|
||||
x: type[Any] = A
|
||||
x: type[Any] = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Bare type
|
||||
|
||||
The interpretation of bare `type` is not clear: existing wording in the spec does not match the
|
||||
behavior of mypy or pyright. For now we interpret it as simply "an instance of `builtins.type`",
|
||||
which is equivalent to `type[object]`. This is similar to the current behavior of mypy, and pyright
|
||||
in strict mode.
|
||||
|
||||
```py
|
||||
def f(x: type):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type = object
|
||||
x: type = type
|
||||
x: type = A
|
||||
x: type = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## type[object] != type[Any]
|
||||
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type[object]
|
||||
# TODO: bound method types
|
||||
reveal_type(x.__repr__) # revealed: Literal[__repr__]
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type[object] = object
|
||||
x: type[object] = type
|
||||
x: type[object] = A
|
||||
x: type[object] = A() # error: [invalid-assignment]
|
||||
```
|
||||
@@ -61,8 +61,10 @@ class B: ...
|
||||
```py path=a/test.py
|
||||
import a.b
|
||||
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
def f(c: type[a.b.C]):
|
||||
reveal_type(c) # revealed: type[C]
|
||||
reveal_type(c) # revealed: @Todo(unsupported type[X] special form)
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# `type[Any]`
|
||||
|
||||
This file contains tests for non-fully-static `type[]` types, such as `type[Any]` and
|
||||
`type[Unknown]`.
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f(x: type[Any], y: type[str]):
|
||||
reveal_type(x) # revealed: type[Any]
|
||||
# TODO: could be `<object.__repr__ type> & Any`
|
||||
reveal_type(x.__repr__) # revealed: Any
|
||||
|
||||
# type[str] and type[Any] are assignable to each other
|
||||
a: type[str] = x
|
||||
b: type[Any] = y
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type[Any] = object
|
||||
x: type[Any] = type
|
||||
x: type[Any] = A
|
||||
x: type[Any] = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Bare type
|
||||
|
||||
The interpretation of bare `type` is not clear: existing wording in the spec does not match the
|
||||
behavior of mypy or pyright. For now we interpret it as simply "an instance of `builtins.type`",
|
||||
which is equivalent to `type[object]`. This is similar to the current behavior of mypy, and pyright
|
||||
in strict mode.
|
||||
|
||||
```py
|
||||
def f(x: type):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type = object
|
||||
x: type = type
|
||||
x: type = A
|
||||
x: type = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## type[object] != type[Any]
|
||||
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type[object]
|
||||
# TODO: bound method types
|
||||
reveal_type(x.__repr__) # revealed: Literal[__repr__]
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type[object] = object
|
||||
x: type[object] = type
|
||||
x: type[object] = A
|
||||
x: type[object] = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## The type of `Any` is `type[Any]`
|
||||
|
||||
`Any` represents an unknown set of possible runtime values. If `x` is of type `Any`, the type of
|
||||
`x.__class__` is also unknown and remains dynamic, *except* that we know it must be a class object
|
||||
of some kind. As such, the type of `x.__class__` is `type[Any]` rather than `Any`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from does_not_exist import SomethingUnknown # error: [unresolved-import]
|
||||
|
||||
reveal_type(SomethingUnknown) # revealed: Unknown
|
||||
|
||||
def test(x: Any, y: SomethingUnknown):
|
||||
reveal_type(x.__class__) # revealed: type[Any]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Any]
|
||||
reveal_type(y.__class__) # revealed: type[Unknown]
|
||||
reveal_type(y.__class__.__class__.__class__.__class__) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## `type[Unknown]` has similar properties to `type[Any]`
|
||||
|
||||
```py
|
||||
import abc
|
||||
from typing import Any
|
||||
from does_not_exist import SomethingUnknown # error: [unresolved-import]
|
||||
|
||||
has_unknown_type = SomethingUnknown.__class__
|
||||
reveal_type(has_unknown_type) # revealed: type[Unknown]
|
||||
|
||||
def test(x: type[str], y: type[Any]):
|
||||
"""Both `type[Any]` and `type[Unknown]` are assignable to all `type[]` types"""
|
||||
a: type[Any] = x
|
||||
b: type[str] = y
|
||||
c: type[Any] = has_unknown_type
|
||||
d: type[str] = has_unknown_type
|
||||
|
||||
def test2(a: type[Any]):
|
||||
"""`type[Any]` and `type[Unknown]` are also assignable to all instances of `type` subclasses"""
|
||||
b: abc.ABCMeta = a
|
||||
b: abc.ABCMeta = has_unknown_type
|
||||
```
|
||||
@@ -9,7 +9,7 @@ from typing import Type
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(c: Type, d: Type[A]):
|
||||
def _(c: Type, d: Type[A], e: Type[A]):
|
||||
reveal_type(c) # revealed: type
|
||||
reveal_type(d) # revealed: type[A]
|
||||
c = d # fine
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
# Custom unary operations
|
||||
|
||||
## Class instances
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
reveal_type(+Yes()) # revealed: bool
|
||||
reveal_type(-Yes()) # revealed: str
|
||||
reveal_type(~Yes()) # revealed: int
|
||||
|
||||
reveal_type(+Sub()) # revealed: bool
|
||||
reveal_type(-Sub()) # revealed: str
|
||||
reveal_type(~Sub()) # revealed: int
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`"
|
||||
reveal_type(+No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`"
|
||||
reveal_type(-No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`"
|
||||
reveal_type(~No()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`"
|
||||
reveal_type(+Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`"
|
||||
reveal_type(-Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`"
|
||||
reveal_type(~Yes) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`"
|
||||
reveal_type(+Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`"
|
||||
reveal_type(-Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`"
|
||||
reveal_type(~Sub) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
|
||||
reveal_type(+No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
|
||||
reveal_type(-No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
|
||||
reveal_type(~No) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function literals
|
||||
|
||||
```py
|
||||
def f():
|
||||
pass
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[f]`"
|
||||
reveal_type(+f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[f]`"
|
||||
reveal_type(-f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[f]`"
|
||||
reveal_type(~f) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
```py
|
||||
class Yes:
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
def yes() -> type[Yes]:
|
||||
return Yes
|
||||
|
||||
def sub() -> type[Sub]:
|
||||
return Sub
|
||||
|
||||
def no() -> type[No]:
|
||||
return No
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Yes]`"
|
||||
reveal_type(+yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`"
|
||||
reveal_type(-yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`"
|
||||
reveal_type(~yes()) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`"
|
||||
reveal_type(+sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`"
|
||||
reveal_type(-sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`"
|
||||
reveal_type(~sub()) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`"
|
||||
reveal_type(+no()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`"
|
||||
reveal_type(-no()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`"
|
||||
reveal_type(~no()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Metaclass
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
def __pos__(self) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self) -> str:
|
||||
return "negative"
|
||||
|
||||
def __invert__(self) -> int:
|
||||
return 17
|
||||
|
||||
class Yes(metaclass=Meta): ...
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
reveal_type(+Yes) # revealed: bool
|
||||
reveal_type(-Yes) # revealed: str
|
||||
reveal_type(~Yes) # revealed: int
|
||||
|
||||
reveal_type(+Sub) # revealed: bool
|
||||
reveal_type(-Sub) # revealed: str
|
||||
reveal_type(~Sub) # revealed: int
|
||||
|
||||
# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
|
||||
reveal_type(+No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
|
||||
reveal_type(-No) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
|
||||
reveal_type(~No) # revealed: Unknown
|
||||
```
|
||||
@@ -17,5 +17,5 @@ class Manager:
|
||||
|
||||
async def test():
|
||||
async with Manager() as f:
|
||||
reveal_type(f) # revealed: @Todo(async `with` statement)
|
||||
reveal_type(f) # revealed: @Todo(async with statement)
|
||||
```
|
||||
|
||||
@@ -27,7 +27,6 @@ pub(crate) mod tests {
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
@@ -167,12 +166,12 @@ pub(crate) mod tests {
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(src_root);
|
||||
search_paths.typeshed = self.custom_typeshed;
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: self.python_version,
|
||||
target_version: self.python_version,
|
||||
search_paths,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -186,26 +186,6 @@ impl ModuleName {
|
||||
self.0.push('.');
|
||||
self.0.push_str(other);
|
||||
}
|
||||
|
||||
/// Returns an iterator of this module name and all of its parent modules.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::<Vec<_>>(),
|
||||
/// vec![
|
||||
/// ModuleName::new_static("foo.bar.baz").unwrap(),
|
||||
/// ModuleName::new_static("foo.bar").unwrap(),
|
||||
/// ModuleName::new_static("foo").unwrap(),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = Self> {
|
||||
std::iter::successors(Some(self.clone()), Self::parent)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ModuleName {
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::path::SearchPath;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
/// Representation of a Python module.
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Module {
|
||||
inner: Arc<ModuleInner>,
|
||||
}
|
||||
@@ -61,7 +61,7 @@ impl std::fmt::Debug for Module {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct ModuleInner {
|
||||
name: ModuleName,
|
||||
kind: ModuleKind,
|
||||
|
||||
@@ -283,9 +283,9 @@ fn query_stdlib_version(
|
||||
let Some(module_name) = stdlib_path_to_module_name(relative_path) else {
|
||||
return TypeshedVersionsQueryResult::DoesNotExist;
|
||||
};
|
||||
let ResolverContext { db, python_version } = context;
|
||||
let ResolverContext { db, target_version } = context;
|
||||
|
||||
typeshed_versions(*db).query_module(&module_name, *python_version)
|
||||
typeshed_versions(*db).query_module(&module_name, *target_version)
|
||||
}
|
||||
|
||||
/// Enumeration describing the various ways in which validation of a search path might fail.
|
||||
@@ -658,7 +658,7 @@ mod tests {
|
||||
let TestCase {
|
||||
db, src, stdlib, ..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
@@ -779,7 +779,7 @@ mod tests {
|
||||
#[should_panic(expected = "Extension must be `pyi`; got `py`")]
|
||||
fn stdlib_path_invalid_join_py() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
@@ -791,7 +791,7 @@ mod tests {
|
||||
#[should_panic(expected = "Extension must be `pyi`; got `rs`")]
|
||||
fn stdlib_path_invalid_join_rs() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
@@ -822,7 +822,7 @@ mod tests {
|
||||
#[test]
|
||||
fn relativize_stdlib_path_errors() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
|
||||
let root = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap();
|
||||
@@ -867,11 +867,11 @@ mod tests {
|
||||
|
||||
fn typeshed_test_case(
|
||||
typeshed: MockedTypeshed,
|
||||
python_version: PythonVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> (TestDb, SearchPath) {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(typeshed)
|
||||
.with_python_version(python_version)
|
||||
.with_custom_typeshed(typeshed)
|
||||
.with_target_version(target_version)
|
||||
.build();
|
||||
let stdlib = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap();
|
||||
(db, stdlib)
|
||||
|
||||
@@ -73,15 +73,6 @@ enum SystemOrVendoredPathRef<'a> {
|
||||
Vendored(&'a VendoredPath),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SystemOrVendoredPathRef<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SystemOrVendoredPathRef::System(system) => system.fmt(f),
|
||||
SystemOrVendoredPathRef::Vendored(vendored) => vendored.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the module for the file with the given id.
|
||||
///
|
||||
/// Returns `None` if the file is not a module locatable via any of the known search paths.
|
||||
@@ -169,7 +160,7 @@ impl SearchPaths {
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
src_root,
|
||||
typeshed,
|
||||
custom_typeshed,
|
||||
site_packages: site_packages_paths,
|
||||
} = settings;
|
||||
|
||||
@@ -189,13 +180,17 @@ impl SearchPaths {
|
||||
tracing::debug!("Adding first-party search path '{src_root}'");
|
||||
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
|
||||
|
||||
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
|
||||
let typeshed = canonicalize(typeshed, system);
|
||||
tracing::debug!("Adding custom-stdlib search path '{typeshed}'");
|
||||
let (typeshed_versions, stdlib_path) = if let Some(custom_typeshed) = custom_typeshed {
|
||||
let custom_typeshed = canonicalize(custom_typeshed, system);
|
||||
tracing::debug!("Adding custom-stdlib search path '{custom_typeshed}'");
|
||||
|
||||
files.try_add_root(db.upcast(), &typeshed, FileRootKind::LibrarySearchPath);
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
&custom_typeshed,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
|
||||
let versions_path = typeshed.join("stdlib/VERSIONS");
|
||||
let versions_path = custom_typeshed.join("stdlib/VERSIONS");
|
||||
|
||||
let versions_content = system.read_to_string(&versions_path).map_err(|error| {
|
||||
SearchPathValidationError::FailedToReadVersionsFile {
|
||||
@@ -206,7 +201,7 @@ impl SearchPaths {
|
||||
|
||||
let parsed: TypeshedVersions = versions_content.parse()?;
|
||||
|
||||
let search_path = SearchPath::custom_stdlib(db, &typeshed)?;
|
||||
let search_path = SearchPath::custom_stdlib(db, &custom_typeshed)?;
|
||||
|
||||
(parsed, search_path)
|
||||
} else {
|
||||
@@ -535,10 +530,10 @@ struct ModuleNameIngredient<'db> {
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, ModuleKind)> {
|
||||
let program = Program::get(db);
|
||||
let python_version = program.python_version(db);
|
||||
let resolver_state = ResolverContext::new(db, python_version);
|
||||
let target_version = program.target_version(db);
|
||||
let resolver_state = ResolverContext::new(db, target_version);
|
||||
let is_builtin_module =
|
||||
ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str());
|
||||
ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str());
|
||||
|
||||
for search_path in search_paths(db) {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
@@ -695,12 +690,12 @@ impl PackageKind {
|
||||
|
||||
pub(super) struct ResolverContext<'db> {
|
||||
pub(super) db: &'db dyn Db,
|
||||
pub(super) python_version: PythonVersion,
|
||||
pub(super) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl<'db> ResolverContext<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, python_version: PythonVersion) -> Self {
|
||||
Self { db, python_version }
|
||||
pub(super) fn new(db: &'db dyn Db, target_version: PythonVersion) -> Self {
|
||||
Self { db, target_version }
|
||||
}
|
||||
|
||||
pub(super) fn vendored(&self) -> &VendoredFileSystem {
|
||||
@@ -776,8 +771,8 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
@@ -794,8 +789,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -847,8 +842,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
@@ -892,8 +887,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
@@ -936,8 +931,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY39)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&[
|
||||
@@ -978,8 +973,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY39)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
@@ -1002,8 +997,8 @@ mod tests {
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1027,7 +1022,7 @@ mod tests {
|
||||
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_vendored_typeshed()
|
||||
.with_python_version(PythonVersion::default())
|
||||
.with_target_version(PythonVersion::default())
|
||||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
@@ -1295,11 +1290,11 @@ mod tests {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::PY38,
|
||||
target_version: PythonVersion::PY38,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
typeshed: Some(custom_typeshed),
|
||||
custom_typeshed: Some(custom_typeshed),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
},
|
||||
@@ -1338,7 +1333,7 @@ mod tests {
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
@@ -1425,8 +1420,8 @@ mod tests {
|
||||
site_packages,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1473,8 +1468,8 @@ mod tests {
|
||||
src,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1513,8 +1508,8 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1800,11 +1795,11 @@ not_a_directory
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
typeshed: None,
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![
|
||||
venv_site_packages,
|
||||
system_site_packages,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub(crate) struct TestCase<T> {
|
||||
// so this is a single directory instead of a `Vec` of directories,
|
||||
// like it is in `ruff_db::Program`.
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) python_version: PythonVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
/// A `(file_name, file_contents)` tuple
|
||||
@@ -67,7 +67,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// ```rs
|
||||
/// let test_case = TestCaseBuilder::new()
|
||||
/// .with_src_files(...)
|
||||
/// .with_python_version(...)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
@@ -85,13 +85,13 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// const TYPESHED = MockedTypeshed { ... };
|
||||
///
|
||||
/// let test_case = resolver_test_case()
|
||||
/// .with_mocked_typeshed(TYPESHED)
|
||||
/// .with_python_version(...)
|
||||
/// .with_custom_typeshed(TYPESHED)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
///
|
||||
/// let test_case2 = resolver_test_case()
|
||||
/// .with_vendored_typeshed()
|
||||
/// .with_python_version(...)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
@@ -100,7 +100,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// to `()`.
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
python_version: PythonVersion,
|
||||
target_version: PythonVersion,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
@@ -118,9 +118,9 @@ impl<T> TestCaseBuilder<T> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the Python version the module resolver should assume
|
||||
pub(crate) fn with_python_version(mut self, python_version: PythonVersion) -> Self {
|
||||
self.python_version = python_version;
|
||||
/// Specify the target Python version the module resolver should assume
|
||||
pub(crate) fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
python_version: PythonVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
@@ -156,33 +156,33 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn with_vendored_typeshed(self) -> TestCaseBuilder<VendoredTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a mock typeshed directory for this test case
|
||||
pub(crate) fn with_mocked_typeshed(
|
||||
pub(crate) fn with_custom_typeshed(
|
||||
self,
|
||||
typeshed: MockedTypeshed,
|
||||
) -> TestCaseBuilder<MockedTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
|
||||
TestCaseBuilder {
|
||||
typeshed_option: typeshed,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
@@ -194,15 +194,15 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
src,
|
||||
stdlib: _,
|
||||
site_packages,
|
||||
python_version,
|
||||
} = self.with_mocked_typeshed(MockedTypeshed::default()).build();
|
||||
target_version,
|
||||
} = self.with_custom_typeshed(MockedTypeshed::default()).build();
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
stdlib: (),
|
||||
site_packages,
|
||||
python_version,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
pub(crate) fn build(self) -> TestCase<SystemPathBuf> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -226,11 +226,11 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
target_version,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
typeshed: Some(typeshed.clone()),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
},
|
||||
},
|
||||
@@ -242,7 +242,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
src,
|
||||
stdlib: typeshed.join("stdlib"),
|
||||
site_packages,
|
||||
python_version,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
pub(crate) fn build(self) -> TestCase<VendoredPathBuf> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -282,7 +282,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
target_version,
|
||||
search_paths: SearchPathSettings {
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
..SearchPathSettings::new(src.clone())
|
||||
@@ -296,7 +296,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
src,
|
||||
stdlib: VendoredPathBuf::from("stdlib"),
|
||||
site_packages,
|
||||
python_version,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +112,10 @@ impl TypeshedVersions {
|
||||
pub(in crate::module_resolver) fn query_module(
|
||||
&self,
|
||||
module: &ModuleName,
|
||||
python_version: PythonVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
if let Some(range) = self.exact(module) {
|
||||
if range.contains(python_version) {
|
||||
if range.contains(target_version) {
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
} else {
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
@@ -125,7 +125,7 @@ impl TypeshedVersions {
|
||||
while let Some(module_to_try) = module {
|
||||
if let Some(range) = self.exact(&module_to_try) {
|
||||
return {
|
||||
if range.contains(python_version) {
|
||||
if range.contains(target_version) {
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
} else {
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::Db;
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
pub struct Program {
|
||||
pub python_version: PythonVersion,
|
||||
pub target_version: PythonVersion,
|
||||
|
||||
#[return_ref]
|
||||
pub(crate) search_paths: SearchPaths,
|
||||
@@ -19,16 +19,16 @@ pub struct Program {
|
||||
impl Program {
|
||||
pub fn from_settings(db: &dyn Db, settings: &ProgramSettings) -> anyhow::Result<Self> {
|
||||
let ProgramSettings {
|
||||
python_version,
|
||||
target_version,
|
||||
search_paths,
|
||||
} = settings;
|
||||
|
||||
tracing::info!("Python version: Python {python_version}");
|
||||
tracing::info!("Target version: Python {target_version}");
|
||||
|
||||
let search_paths = SearchPaths::from_settings(db, search_paths)
|
||||
.with_context(|| "Invalid search path settings")?;
|
||||
|
||||
Ok(Program::builder(settings.python_version, search_paths)
|
||||
Ok(Program::builder(settings.target_version, search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db))
|
||||
}
|
||||
@@ -56,7 +56,7 @@ impl Program {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct ProgramSettings {
|
||||
pub python_version: PythonVersion,
|
||||
pub target_version: PythonVersion,
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub struct SearchPathSettings {
|
||||
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub typeshed: Option<SystemPathBuf>,
|
||||
pub custom_typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub site_packages: SitePackages,
|
||||
@@ -86,7 +86,7 @@ impl SearchPathSettings {
|
||||
Self {
|
||||
src_root,
|
||||
extra_paths: vec![],
|
||||
typeshed: None,
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
@@ -67,42 +68,3 @@ impl fmt::Display for PythonVersion {
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for PythonVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let as_str = String::deserialize(deserializer)?;
|
||||
|
||||
if let Some((major, minor)) = as_str.split_once('.') {
|
||||
let major = major
|
||||
.parse()
|
||||
.map_err(|err| serde::de::Error::custom(format!("invalid major version: {err}")))?;
|
||||
let minor = minor
|
||||
.parse()
|
||||
.map_err(|err| serde::de::Error::custom(format!("invalid minor version: {err}")))?;
|
||||
|
||||
Ok((major, minor).into())
|
||||
} else {
|
||||
let major = as_str.parse().map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"invalid python-version: {err}, expected: `major.minor`"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok((major, 0).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for PythonVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_index::{IndexSlice, IndexVec};
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIds;
|
||||
use crate::semantic_index::builder::SemanticIndexBuilder;
|
||||
@@ -61,22 +60,6 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
|
||||
index.symbol_table(scope.file_scope_id(db))
|
||||
}
|
||||
|
||||
/// Returns the set of modules that are imported anywhere in `file`.
|
||||
///
|
||||
/// This set only considers `import` statements, not `from...import` statements, because:
|
||||
///
|
||||
/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is
|
||||
/// therefore imported) without looking outside the content of this file. (We could turn this
|
||||
/// into a _potentially_ imported modules set, but that would change how it's used in our type
|
||||
/// inference logic.)
|
||||
///
|
||||
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
|
||||
/// knowing the name of the current module, and whether it's a package.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
|
||||
semantic_index(db, file).imported_modules.clone()
|
||||
}
|
||||
|
||||
/// Returns the use-def map for a specific `scope`.
|
||||
///
|
||||
/// Using [`use_def_map`] over [`semantic_index`] has the advantage that
|
||||
@@ -133,9 +116,6 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// changing a file invalidates all dependents.
|
||||
ast_ids: IndexVec<FileScopeId, AstIds>,
|
||||
|
||||
/// The set of modules that are imported anywhere within this file.
|
||||
imported_modules: Arc<FxHashSet<ModuleName>>,
|
||||
|
||||
/// Flags about the global scope (code usage impacting inference)
|
||||
has_future_annotations: bool,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use except_handlers::TryNodeContextStackManager;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
@@ -12,7 +12,6 @@ use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
||||
use ruff_python_ast::{BoolOp, Expr};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::definition::{
|
||||
@@ -80,7 +79,6 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndexBuilder<'db> {
|
||||
@@ -107,8 +105,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
definitions_by_node: FxHashMap::default(),
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
|
||||
imported_modules: FxHashSet::default(),
|
||||
};
|
||||
|
||||
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
|
||||
@@ -562,7 +558,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scopes_by_expression: self.scopes_by_expression,
|
||||
scopes_by_node: self.scopes_by_node,
|
||||
use_def_maps,
|
||||
imported_modules: Arc::new(self.imported_modules),
|
||||
has_future_annotations: self.has_future_annotations,
|
||||
}
|
||||
}
|
||||
@@ -666,12 +661,6 @@ where
|
||||
}
|
||||
ast::Stmt::Import(node) => {
|
||||
for alias in &node.names {
|
||||
// Mark the imported module, and all of its parents, as being imported in this
|
||||
// file.
|
||||
if let Some(module_name) = ModuleName::new(&alias.name) {
|
||||
self.imported_modules.extend(module_name.ancestors());
|
||||
}
|
||||
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
asname.id.clone()
|
||||
} else {
|
||||
@@ -844,7 +833,6 @@ where
|
||||
self.visit_expr(test);
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
@@ -864,7 +852,6 @@ where
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(pre_loop);
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_body(orelse);
|
||||
|
||||
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_resolver::file_to_module;
|
||||
@@ -466,33 +465,6 @@ pub enum DefinitionKind<'db> {
|
||||
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
|
||||
}
|
||||
|
||||
impl Ranged for DefinitionKind<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(alias) => alias.range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
DefinitionKind::Function(function) => function.name.range(),
|
||||
DefinitionKind::Class(class) => class.name.range(),
|
||||
DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(),
|
||||
DefinitionKind::NamedExpression(named) => named.target.range(),
|
||||
DefinitionKind::Assignment(assignment) => assignment.name().range(),
|
||||
DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
|
||||
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.target.range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.target().range(),
|
||||
DefinitionKind::Comprehension(comp) => comp.target().range(),
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::Parameter(parameter) => parameter.parameter.name.range(),
|
||||
DefinitionKind::WithItem(with_item) => with_item.target().range(),
|
||||
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
|
||||
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
|
||||
DefinitionKind::TypeVar(type_var) => type_var.name.range(),
|
||||
DefinitionKind::ParamSpec(param_spec) => param_spec.name.range(),
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.name.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
|
||||
@@ -321,7 +321,7 @@ fn site_packages_directory_from_sys_prefix(
|
||||
// the parsed version
|
||||
//
|
||||
// Note: the `python3.x` part of the `site-packages` path can't be computed from
|
||||
// the `--python-version` the user has passed, as they might be running Python 3.12 locally
|
||||
// the `--target-version` the user has passed, as they might be running Python 3.12 locally
|
||||
// even if they've requested that we type check their code "as if" they're running 3.8.
|
||||
for entry_result in system
|
||||
.read_directory(&sys_prefix_path.join("lib"))
|
||||
|
||||
@@ -15,9 +15,6 @@ pub(crate) enum CoreStdlibModule {
|
||||
TypingExtensions,
|
||||
Typing,
|
||||
Sys,
|
||||
#[allow(dead_code)]
|
||||
Abc, // currently only used in tests
|
||||
Collections,
|
||||
}
|
||||
|
||||
impl CoreStdlibModule {
|
||||
@@ -29,8 +26,6 @@ impl CoreStdlibModule {
|
||||
Self::Typeshed => "_typeshed",
|
||||
Self::TypingExtensions => "typing_extensions",
|
||||
Self::Sys => "sys",
|
||||
Self::Abc => "abc",
|
||||
Self::Collections => "collections",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use salsa;
|
||||
|
||||
use ruff_db::{files::File, parsed::comment_ranges, source::source_text};
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
|
||||
use crate::{lint::LintId, Db};
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn suppressions(db: &dyn Db, file: File) -> IndexVec<SuppressionIndex, Suppression> {
|
||||
let comments = comment_ranges(db.upcast(), file);
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
let mut suppressions = IndexVec::default();
|
||||
|
||||
for range in comments {
|
||||
let text = &source[range];
|
||||
|
||||
if text.starts_with("# type: ignore") {
|
||||
suppressions.push(Suppression {
|
||||
target: None,
|
||||
kind: SuppressionKind::TypeIgnore,
|
||||
});
|
||||
} else if text.starts_with("# knot: ignore") {
|
||||
suppressions.push(Suppression {
|
||||
target: None,
|
||||
kind: SuppressionKind::KnotIgnore,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
suppressions
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub(crate) struct SuppressionIndex;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct Suppression {
|
||||
target: Option<LintId>,
|
||||
kind: SuppressionKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum SuppressionKind {
|
||||
/// A `type: ignore` comment
|
||||
TypeIgnore,
|
||||
|
||||
/// A `knot: ignore` comment
|
||||
KnotIgnore,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,8 +30,6 @@ use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::Truthiness;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -245,22 +243,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
}
|
||||
} else {
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
// ~AlwaysTruthy & bool = Literal[False]
|
||||
if let Type::Instance(InstanceType { class }) = new_positive {
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
if let Some(new_type) = self
|
||||
if let Some(&Type::BooleanLiteral(value)) = self
|
||||
.negative
|
||||
.iter()
|
||||
.find(|element| {
|
||||
element.is_boolean_literal()
|
||||
| matches!(element, Type::AlwaysFalsy | Type::AlwaysTruthy)
|
||||
})
|
||||
.map(|element| {
|
||||
Type::BooleanLiteral(element.bool(db) != Truthiness::AlwaysTrue)
|
||||
})
|
||||
.find(|element| element.is_boolean_literal())
|
||||
{
|
||||
*self = Self::default();
|
||||
self.positive.insert(new_type);
|
||||
self.positive.insert(Type::BooleanLiteral(!value));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -327,15 +318,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
// simplify the representation.
|
||||
self.add_positive(db, ty);
|
||||
}
|
||||
// bool & ~Literal[True] = Literal[False]
|
||||
// bool & ~AlwaysTruthy = Literal[False]
|
||||
Type::BooleanLiteral(_) | Type::AlwaysFalsy | Type::AlwaysTruthy
|
||||
if self.positive.contains(&KnownClass::Bool.to_instance(db)) =>
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
Type::BooleanLiteral(bool)
|
||||
if self
|
||||
.positive
|
||||
.iter()
|
||||
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
|
||||
{
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::BooleanLiteral(
|
||||
new_negative.bool(db) != Truthiness::AlwaysTrue,
|
||||
));
|
||||
self.positive.insert(Type::BooleanLiteral(!bool));
|
||||
}
|
||||
_ => {
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
@@ -389,7 +380,7 @@ mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
@@ -1006,43 +997,42 @@ mod tests {
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test_case(Type::BooleanLiteral(true))]
|
||||
#[test_case(Type::BooleanLiteral(false))]
|
||||
#[test_case(Type::AlwaysTruthy)]
|
||||
#[test_case(Type::AlwaysFalsy)]
|
||||
fn build_intersection_simplify_split_bool(t_splitter: Type) {
|
||||
#[test_case(true)]
|
||||
#[test_case(false)]
|
||||
fn build_intersection_simplify_split_bool(bool_value: bool) {
|
||||
let db = setup_db();
|
||||
let bool_value = t_splitter.bool(&db) == Truthiness::AlwaysTrue;
|
||||
|
||||
let t_bool = KnownClass::Bool.to_instance(&db);
|
||||
let t_boolean_literal = Type::BooleanLiteral(bool_value);
|
||||
|
||||
// We add t_object in various orders (in first or second position) in
|
||||
// the tests below to ensure that the boolean simplification eliminates
|
||||
// everything from the intersection, not just `bool`.
|
||||
let t_object = KnownClass::Object.to_instance(&db);
|
||||
let t_bool = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_object)
|
||||
.add_positive(t_bool)
|
||||
.add_negative(t_splitter)
|
||||
.add_negative(t_boolean_literal)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_bool)
|
||||
.add_positive(t_object)
|
||||
.add_negative(t_splitter)
|
||||
.add_negative(t_boolean_literal)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_object)
|
||||
.add_negative(t_splitter)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_positive(t_bool)
|
||||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_splitter)
|
||||
.add_negative(t_boolean_literal)
|
||||
.add_positive(t_object)
|
||||
.add_positive(t_bool)
|
||||
.build();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::context::InferContext;
|
||||
use super::diagnostic::CALL_NON_CALLABLE;
|
||||
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use super::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
|
||||
use super::{Severity, Type, UnionBuilder};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_python_ast as ast;
|
||||
@@ -87,23 +86,24 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Get the return type of the call, emitting default diagnostics if needed.
|
||||
pub(super) fn unwrap_with_diagnostic(
|
||||
pub(super) fn unwrap_with_diagnostic<'a>(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
db: &'db dyn Db,
|
||||
node: ast::AnyNodeRef,
|
||||
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
|
||||
) -> Type<'db> {
|
||||
match self.return_ty_result(context, node) {
|
||||
match self.return_ty_result(db, node, diagnostics) {
|
||||
Ok(return_ty) => return_ty,
|
||||
Err(NotCallableError::Type {
|
||||
not_callable_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_ty.display(context.db())
|
||||
not_callable_ty.display(db)
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -113,13 +113,13 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union element `{}`)",
|
||||
called_ty.display(context.db()),
|
||||
not_callable_ty.display(context.db()),
|
||||
called_ty.display(db),
|
||||
not_callable_ty.display(db),
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -129,13 +129,13 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union elements {})",
|
||||
called_ty.display(context.db()),
|
||||
not_callable_tys.display(context.db()),
|
||||
called_ty.display(db),
|
||||
Type::display_slice(db, ¬_callable_tys),
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -144,12 +144,12 @@ impl<'db> CallOutcome<'db> {
|
||||
callable_ty: called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
called_ty.display(context.db())
|
||||
called_ty.display(db)
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
@@ -158,10 +158,11 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
|
||||
/// Get the return type of the call as a result.
|
||||
pub(super) fn return_ty_result(
|
||||
pub(super) fn return_ty_result<'a>(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
db: &'db dyn Db,
|
||||
node: ast::AnyNodeRef,
|
||||
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
|
||||
) -> Result<Type<'db>, NotCallableError<'db>> {
|
||||
match self {
|
||||
Self::Callable { return_ty } => Ok(*return_ty),
|
||||
@@ -169,11 +170,11 @@ impl<'db> CallOutcome<'db> {
|
||||
return_ty,
|
||||
revealed_ty,
|
||||
} => {
|
||||
context.report_diagnostic(
|
||||
diagnostics.add(
|
||||
node,
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
|
||||
);
|
||||
Ok(*return_ty)
|
||||
}
|
||||
@@ -186,16 +187,14 @@ impl<'db> CallOutcome<'db> {
|
||||
call_outcome,
|
||||
} => Err(NotCallableError::PossiblyUnboundDunderCall {
|
||||
callable_ty: *called_ty,
|
||||
return_ty: call_outcome
|
||||
.return_ty(context.db())
|
||||
.unwrap_or(Type::Unknown),
|
||||
return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown),
|
||||
}),
|
||||
Self::Union {
|
||||
outcomes,
|
||||
called_ty,
|
||||
} => {
|
||||
let mut not_callable = vec![];
|
||||
let mut union_builder = UnionBuilder::new(context.db());
|
||||
let mut union_builder = UnionBuilder::new(db);
|
||||
let mut revealed = false;
|
||||
for outcome in outcomes {
|
||||
let return_ty = match outcome {
|
||||
@@ -211,10 +210,10 @@ impl<'db> CallOutcome<'db> {
|
||||
*return_ty
|
||||
} else {
|
||||
revealed = true;
|
||||
outcome.unwrap_with_diagnostic(context, node)
|
||||
outcome.unwrap_with_diagnostic(db, node, diagnostics)
|
||||
}
|
||||
}
|
||||
_ => outcome.unwrap_with_diagnostic(context, node),
|
||||
_ => outcome.unwrap_with_diagnostic(db, node, diagnostics),
|
||||
};
|
||||
union_builder = union_builder.add(return_ty);
|
||||
}
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
use crate::types::{
|
||||
todo_type, Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Either;
|
||||
|
||||
/// Enumeration of the possible kinds of types we allow in class bases.
|
||||
///
|
||||
/// This is much more limited than the [`Type`] enum:
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::Unknown`]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum ClassBase<'db> {
|
||||
Any,
|
||||
Unknown,
|
||||
Todo(TodoType),
|
||||
Class(Class<'db>),
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
struct Display<'db> {
|
||||
base: ClassBase<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Any => f.write_str("Any"),
|
||||
ClassBase::Todo(todo) => todo.fmt(f),
|
||||
ClassBase::Unknown => f.write_str("Unknown"),
|
||||
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Display { base: self, db }
|
||||
}
|
||||
|
||||
/// Return a `ClassBase` representing the class `builtins.object`
|
||||
pub(super) fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.into_class_literal()
|
||||
.map_or(Self::Unknown, |ClassLiteralType { class }| {
|
||||
Self::Class(class)
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to resolve `ty` into a `ClassBase`.
|
||||
///
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
pub(super) fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
Type::Todo(todo) => Some(Self::Todo(todo)),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::TypeVar(_)
|
||||
| KnownInstanceType::TypeAliasType(_)
|
||||
| KnownInstanceType::Annotated
|
||||
| KnownInstanceType::Literal
|
||||
| KnownInstanceType::LiteralString
|
||||
| KnownInstanceType::Union
|
||||
| KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Final
|
||||
| KnownInstanceType::NotRequired
|
||||
| KnownInstanceType::TypeGuard
|
||||
| KnownInstanceType::TypeIs
|
||||
| KnownInstanceType::TypingSelf
|
||||
| KnownInstanceType::Unpack
|
||||
| KnownInstanceType::ClassVar
|
||||
| KnownInstanceType::Concatenate
|
||||
| KnownInstanceType::Required
|
||||
| KnownInstanceType::TypeAlias
|
||||
| KnownInstanceType::ReadOnly
|
||||
| KnownInstanceType::Optional => None,
|
||||
KnownInstanceType::Any => Some(Self::Any),
|
||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||
KnownInstanceType::Dict => {
|
||||
Self::try_from_ty(db, KnownClass::Dict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::List => {
|
||||
Self::try_from_ty(db, KnownClass::List.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Type => {
|
||||
Self::try_from_ty(db, KnownClass::Type.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Tuple => {
|
||||
Self::try_from_ty(db, KnownClass::Tuple.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Set => {
|
||||
Self::try_from_ty(db, KnownClass::Set.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::FrozenSet => {
|
||||
Self::try_from_ty(db, KnownClass::FrozenSet.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::ChainMap => {
|
||||
Self::try_from_ty(db, KnownClass::ChainMap.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Counter => {
|
||||
Self::try_from_ty(db, KnownClass::Counter.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::DefaultDict => {
|
||||
Self::try_from_ty(db, KnownClass::DefaultDict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Deque => {
|
||||
Self::try_from_ty(db, KnownClass::Deque.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::OrderedDict => {
|
||||
Self::try_from_ty(db, KnownClass::OrderedDict.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Callable => {
|
||||
Self::try_from_ty(db, todo_type!("Support for Callable as a base class"))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn into_class(self) -> Option<Class<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the MRO of this base
|
||||
pub(super) fn mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
|
||||
match self {
|
||||
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Unknown => {
|
||||
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Todo(todo) => {
|
||||
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Class<'db>> for ClassBase<'db> {
|
||||
fn from(value: Class<'db>) -> Self {
|
||||
ClassBase::Class(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
ClassBase::Any => Type::Any,
|
||||
ClassBase::Todo(todo) => Type::Todo(todo),
|
||||
ClassBase::Unknown => Type::Unknown,
|
||||
ClassBase::Class(class) => Type::class_literal(class),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<&ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: &ClassBase<'db>) -> Self {
|
||||
Self::from(*value)
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use drop_bomb::DebugDropBomb;
|
||||
use ruff_db::{
|
||||
diagnostic::{DiagnosticId, Severity},
|
||||
files::File,
|
||||
};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{
|
||||
lint::{LintId, LintMetadata},
|
||||
Db,
|
||||
};
|
||||
|
||||
use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
|
||||
/// Context for inferring the types of a single file.
|
||||
///
|
||||
/// One context exists for at least for every inferred region but it's
|
||||
/// possible that inferring a sub-region, like an unpack assignment, creates
|
||||
/// a sub-context.
|
||||
///
|
||||
/// Tracks the reported diagnostics of the inferred region.
|
||||
///
|
||||
/// ## Consuming
|
||||
/// It's important that the context is explicitly consumed before dropping by calling
|
||||
/// [`InferContext::finish`] and the returned diagnostics must be stored
|
||||
/// on the current [`TypeInference`](super::infer::TypeInference) result.
|
||||
pub(crate) struct InferContext<'db> {
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
diagnostics: std::cell::RefCell<TypeCheckDiagnostics>,
|
||||
bomb: DebugDropBomb,
|
||||
}
|
||||
|
||||
impl<'db> InferContext<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()),
|
||||
bomb: DebugDropBomb::new("`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics."),
|
||||
}
|
||||
}
|
||||
|
||||
/// The file for which the types are inferred.
|
||||
pub(crate) fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
pub(crate) fn db(&self) -> &'db dyn Db {
|
||||
self.db
|
||||
}
|
||||
|
||||
pub(crate) fn extend<T>(&mut self, other: &T)
|
||||
where
|
||||
T: WithDiagnostics,
|
||||
{
|
||||
self.diagnostics
|
||||
.get_mut()
|
||||
.extend(other.diagnostics().iter().cloned());
|
||||
}
|
||||
|
||||
/// Reports a lint located at `node`.
|
||||
pub(super) fn report_lint(
|
||||
&self,
|
||||
lint: &'static LintMetadata,
|
||||
node: AnyNodeRef,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
// Skip over diagnostics if the rule is disabled.
|
||||
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.report_diagnostic(node, DiagnosticId::Lint(lint.name()), severity, message);
|
||||
}
|
||||
|
||||
/// Adds a new diagnostic.
|
||||
///
|
||||
/// The diagnostic does not get added if the rule isn't enabled for this file.
|
||||
pub(super) fn report_diagnostic(
|
||||
&self,
|
||||
node: AnyNodeRef,
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: std::fmt::Arguments,
|
||||
) {
|
||||
if !self.db.is_file_open(self.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Don't emit the diagnostic if:
|
||||
// * The enclosing node contains any syntax errors
|
||||
// * The rule is disabled for this file. We probably want to introduce a new query that
|
||||
// returns a rule selector for a given file that respects the package's settings,
|
||||
// any global pragma comments in the file, and any per-file-ignores.
|
||||
// * Check for suppression comments, bump a counter if the diagnostic is suppressed.
|
||||
|
||||
self.diagnostics.borrow_mut().push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
id,
|
||||
message: message.to_string(),
|
||||
range: node.range(),
|
||||
severity,
|
||||
});
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
|
||||
self.bomb.defuse();
|
||||
let mut diagnostics = self.diagnostics.into_inner();
|
||||
diagnostics.shrink_to_fit();
|
||||
diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for InferContext<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("TyContext")
|
||||
.field("file", &self.file)
|
||||
.field("diagnostics", &self.diagnostics)
|
||||
.field("defused", &self.bomb)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait WithDiagnostics {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,10 @@
|
||||
//! Display implementations for types.
|
||||
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
|
||||
use ruff_db::display::FormatterJoinExtension;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
use std::fmt::{self, Arguments, Formatter, Write};
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::mro::ClassBase;
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
|
||||
SubclassOfType, Type, UnionType,
|
||||
@@ -15,25 +13,33 @@ use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
pub fn display(&self, db: &'db dyn Db) -> DisplayType {
|
||||
DisplayType { ty: self, db }
|
||||
fn representation(self) -> Representation<'db> {
|
||||
Representation { ty: self }
|
||||
}
|
||||
fn representation(self, db: &'db dyn Db) -> DisplayRepresentation<'db> {
|
||||
DisplayRepresentation { db, ty: self }
|
||||
|
||||
pub fn display(self, db: &'db dyn Db) -> DisplayWrapper<'db, Type<'db>> {
|
||||
DisplayWrapper::new(db, self)
|
||||
}
|
||||
|
||||
pub fn display_slice<'types>(
|
||||
db: &'db dyn Db,
|
||||
types: &'types [Type<'db>],
|
||||
) -> DisplayWrapper<'db, &'types [Type<'db>]> {
|
||||
DisplayWrapper::new(db, types)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct DisplayType<'db> {
|
||||
ty: &'db Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
impl<'db> DisplayType<'db> for Type<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
if f.visited.contains(self) {
|
||||
return f.write_str("<recursion>");
|
||||
}
|
||||
f.visited.push(*self);
|
||||
|
||||
let representation = self.representation();
|
||||
|
||||
impl Display for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let representation = self.ty.representation(self.db);
|
||||
if matches!(
|
||||
self.ty,
|
||||
self,
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
@@ -41,76 +47,76 @@ impl Display for DisplayType<'_> {
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
) {
|
||||
write!(f, "Literal[{representation}]")
|
||||
f.write_str("Literal[")?;
|
||||
representation.fmt(f)?;
|
||||
f.write_str("]")?;
|
||||
} else {
|
||||
representation.fmt(f)
|
||||
representation.fmt(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
let removed = f.visited.pop();
|
||||
debug_assert_eq!(removed, Some(*self));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the string representation of a type, which is the value displayed either as
|
||||
/// `Literal[<repr>]` or `Literal[<repr1>, <repr2>]` for literal types or as `<repr>` for
|
||||
/// non literals
|
||||
struct DisplayRepresentation<'db> {
|
||||
struct Representation<'db> {
|
||||
ty: Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayRepresentation<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
impl<'db> DisplayType<'db> for Representation<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
match self.ty {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Instance(InstanceType { class }) => {
|
||||
let representation = match class.known(self.db) {
|
||||
let representation = match class.known(f.db()) {
|
||||
Some(KnownClass::NoneType) => "None",
|
||||
Some(KnownClass::NoDefaultType) => "NoDefault",
|
||||
_ => class.name(self.db),
|
||||
_ => class.name(f.db()),
|
||||
};
|
||||
f.write_str(representation)
|
||||
}
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo(todo) => write!(f, "@Todo{todo}"),
|
||||
Type::ModuleLiteral(module) => {
|
||||
write!(f, "<module '{}'>", module.module(self.db).name())
|
||||
Type::ModuleLiteral(file) => {
|
||||
write!(f, "<module '{:?}'>", file.path(f.db()))
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(f.db())),
|
||||
Type::SubclassOf(SubclassOfType {
|
||||
base: ClassBase::Class(class),
|
||||
}) => {
|
||||
// Only show the bare class name here; ClassBase::display would render this as
|
||||
// type[<class 'Foo'>] instead of type[Foo].
|
||||
write!(f, "type[{}]", class.name(self.db))
|
||||
write!(f, "type[{}]", class.name(f.db()))
|
||||
}
|
||||
Type::SubclassOf(SubclassOfType { base }) => {
|
||||
write!(f, "type[{}]", base.display(self.db))
|
||||
write!(f, "type[{}]", base.display(f.db()))
|
||||
}
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
Type::IntLiteral(n) => n.fmt(f),
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(f.db())),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(f.db())),
|
||||
Type::Union(union) => union.fmt(f),
|
||||
Type::Intersection(intersection) => intersection.fmt(f),
|
||||
Type::IntLiteral(n) => write!(f, "{n}"),
|
||||
Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }),
|
||||
Type::StringLiteral(string) => string.display(self.db).fmt(f),
|
||||
Type::StringLiteral(string) => string.fmt(f),
|
||||
Type::LiteralString => f.write_str("LiteralString"),
|
||||
Type::BytesLiteral(bytes) => {
|
||||
let escape =
|
||||
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
|
||||
AsciiEscape::with_preferred_quote(bytes.value(f.db()).as_ref(), Quote::Double);
|
||||
|
||||
escape.bytes_repr().write(f)
|
||||
}
|
||||
Type::SliceLiteral(slice) => {
|
||||
f.write_str("slice[")?;
|
||||
if let Some(start) = slice.start(self.db) {
|
||||
if let Some(start) = slice.start(f.db()) {
|
||||
write!(f, "Literal[{start}]")?;
|
||||
} else {
|
||||
f.write_str("None")?;
|
||||
@@ -118,13 +124,13 @@ impl Display for DisplayRepresentation<'_> {
|
||||
|
||||
f.write_str(", ")?;
|
||||
|
||||
if let Some(stop) = slice.stop(self.db) {
|
||||
if let Some(stop) = slice.stop(f.db()) {
|
||||
write!(f, "Literal[{stop}]")?;
|
||||
} else {
|
||||
f.write_str("None")?;
|
||||
}
|
||||
|
||||
if let Some(step) = slice.step(self.db) {
|
||||
if let Some(step) = slice.step(f.db()) {
|
||||
write!(f, ", Literal[{step}]")?;
|
||||
}
|
||||
|
||||
@@ -132,34 +138,21 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
Type::Tuple(tuple) => {
|
||||
f.write_str("tuple[")?;
|
||||
let elements = tuple.elements(self.db);
|
||||
let elements = tuple.elements(f.db());
|
||||
if elements.is_empty() {
|
||||
f.write_str("()")?;
|
||||
} else {
|
||||
elements.display(self.db).fmt(f)?;
|
||||
elements.fmt(f)?;
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
|
||||
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UnionType<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> {
|
||||
DisplayUnionType { db, ty: self }
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayUnionType<'db> {
|
||||
ty: &'db UnionType<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let elements = self.ty.elements(self.db);
|
||||
impl<'db> DisplayType<'db> for UnionType<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
let elements = self.elements(f.db());
|
||||
|
||||
// Group condensed-display types by kind.
|
||||
let mut grouped_condensed_kinds = FxHashMap::default();
|
||||
@@ -183,12 +176,11 @@ impl Display for DisplayUnionType<'_> {
|
||||
if kind == CondensedDisplayTypeKind::Int {
|
||||
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
|
||||
}
|
||||
join.entry(&DisplayLiteralGroup {
|
||||
join.entry(&LiteralGroup {
|
||||
literals: condensed_kind,
|
||||
db: self.db,
|
||||
});
|
||||
} else {
|
||||
join.entry(&element.display(self.db));
|
||||
join.entry(element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,22 +192,15 @@ impl Display for DisplayUnionType<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayLiteralGroup<'db> {
|
||||
struct LiteralGroup<'db> {
|
||||
literals: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayLiteralGroup<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
impl<'db> DisplayType<'db> for LiteralGroup<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
f.write_str("Literal[")?;
|
||||
f.join(", ")
|
||||
.entries(self.literals.iter().map(|ty| ty.representation(self.db)))
|
||||
.entries(self.literals.iter().map(|ty| ty.representation()))
|
||||
.finish()?;
|
||||
f.write_str("]")
|
||||
}
|
||||
@@ -251,106 +236,63 @@ impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplayIntersectionType<'db> {
|
||||
DisplayIntersectionType { db, ty: self }
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayIntersectionType<'db> {
|
||||
ty: &'db IntersectionType<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayIntersectionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
impl<'db> DisplayType<'db> for IntersectionType<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
let tys = self
|
||||
.ty
|
||||
.positive(self.db)
|
||||
.positive(f.db())
|
||||
.iter()
|
||||
.map(|&ty| DisplayMaybeNegatedType {
|
||||
ty,
|
||||
db: self.db,
|
||||
negated: false,
|
||||
})
|
||||
.map(|&ty| MaybeNegatedType { ty, negated: false })
|
||||
.chain(
|
||||
self.ty
|
||||
.negative(self.db)
|
||||
self.negative(f.db())
|
||||
.iter()
|
||||
.map(|&ty| DisplayMaybeNegatedType {
|
||||
ty,
|
||||
db: self.db,
|
||||
negated: true,
|
||||
}),
|
||||
.map(|&ty| MaybeNegatedType { ty, negated: true }),
|
||||
);
|
||||
f.join(" & ").entries(tys).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DisplayIntersectionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayMaybeNegatedType<'db> {
|
||||
struct MaybeNegatedType<'db> {
|
||||
ty: Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
negated: bool,
|
||||
}
|
||||
|
||||
impl Display for DisplayMaybeNegatedType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
impl<'db> DisplayType<'db> for MaybeNegatedType<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
if self.negated {
|
||||
f.write_str("~")?;
|
||||
}
|
||||
self.ty.display(self.db).fmt(f)
|
||||
|
||||
self.ty.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TypeArrayDisplay<'db> {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray;
|
||||
}
|
||||
|
||||
impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
|
||||
DisplayTypeArray { types: self, db }
|
||||
impl<'db> DisplayType<'db> for [Type<'db>] {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
f.join(", ").entries(self.iter().copied()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> TypeArrayDisplay<'db> for Vec<Type<'db>> {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
|
||||
DisplayTypeArray { types: self, db }
|
||||
impl<'db> DisplayType<'db> for &[Type<'db>] {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
(**self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayTypeArray<'b, 'db> {
|
||||
types: &'b [Type<'db>],
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayTypeArray<'_, '_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.join(", ")
|
||||
.entries(self.types.iter().map(|ty| ty.display(self.db)))
|
||||
.finish()
|
||||
impl<'db> DisplayType<'db> for Box<[Type<'db>]> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
(**self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> StringLiteralType<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> {
|
||||
DisplayStringLiteralType { db, ty: self }
|
||||
impl<'db> DisplayType<'db> for Vec<Type<'db>> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
(**self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayStringLiteralType<'db> {
|
||||
ty: &'db StringLiteralType<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayStringLiteralType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let value = self.ty.value(self.db);
|
||||
impl<'db> DisplayType<'db> for StringLiteralType<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
let value = self.value(f.db());
|
||||
f.write_char('"')?;
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
@@ -364,6 +306,119 @@ impl Display for DisplayStringLiteralType<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
struct TypeFormatter<'db, 'write> {
|
||||
db: &'db dyn Db,
|
||||
write: &'write mut dyn Write,
|
||||
visited: Vec<Type<'db>>,
|
||||
}
|
||||
|
||||
impl<'db, 'write> TypeFormatter<'db, 'write> {
|
||||
pub(crate) fn new(db: &'db dyn Db, write: &'write mut dyn Write) -> Self {
|
||||
Self {
|
||||
db,
|
||||
write,
|
||||
visited: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn join<'f>(&'f mut self, separator: &'static str) -> Join<'db, 'f, 'write> {
|
||||
Join {
|
||||
fmt: self,
|
||||
separator,
|
||||
result: Ok(()),
|
||||
seen_first: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn db(&self) -> &'db dyn Db {
|
||||
self.db
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for TypeFormatter<'_, '_> {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write.write_str(s)
|
||||
}
|
||||
|
||||
fn write_char(&mut self, c: char) -> fmt::Result {
|
||||
self.write.write_char(c)
|
||||
}
|
||||
|
||||
fn write_fmt(&mut self, args: Arguments<'_>) -> fmt::Result {
|
||||
self.write.write_fmt(args)
|
||||
}
|
||||
}
|
||||
|
||||
trait DisplayType<'db> {
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result;
|
||||
}
|
||||
|
||||
pub struct DisplayWrapper<'db, T> {
|
||||
db: &'db dyn Db,
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<'db, T> DisplayWrapper<'db, T> {
|
||||
fn new(db: &'db dyn Db, inner: T) -> Self {
|
||||
Self { db, inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, T> DisplayType<'db> for DisplayWrapper<'db, T>
|
||||
where
|
||||
T: DisplayType<'db>,
|
||||
{
|
||||
fn fmt(&self, f: &mut TypeFormatter<'db, '_>) -> fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, T> fmt::Display for DisplayWrapper<'db, T>
|
||||
where
|
||||
T: DisplayType<'db>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let mut f = TypeFormatter::new(self.db, f);
|
||||
DisplayType::fmt(self, &mut f)
|
||||
}
|
||||
}
|
||||
|
||||
struct Join<'db, 'f, 'write> {
|
||||
fmt: &'f mut TypeFormatter<'db, 'write>,
|
||||
separator: &'static str,
|
||||
result: fmt::Result,
|
||||
seen_first: bool,
|
||||
}
|
||||
|
||||
impl<'db> Join<'db, '_, '_> {
|
||||
fn entry(&mut self, item: &dyn DisplayType<'db>) -> &mut Self {
|
||||
if self.seen_first {
|
||||
self.result = self
|
||||
.result
|
||||
.and_then(|()| self.fmt.write_str(self.separator));
|
||||
} else {
|
||||
self.seen_first = true;
|
||||
}
|
||||
self.result = self.result.and_then(|()| item.fmt(self.fmt));
|
||||
self
|
||||
}
|
||||
|
||||
fn entries<I, F>(&mut self, items: I) -> &mut Self
|
||||
where
|
||||
I: IntoIterator<Item = F>,
|
||||
F: DisplayType<'db>,
|
||||
{
|
||||
for item in items {
|
||||
self.entry(&item);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> fmt::Result {
|
||||
self.result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
@@ -408,7 +463,7 @@ mod tests {
|
||||
Type::none(&db),
|
||||
];
|
||||
let union = UnionType::from_elements(&db, union_elements).expect_union();
|
||||
let display = format!("{}", union.display(&db));
|
||||
let display = format!("{}", Type::Union(union).display(&db));
|
||||
assert_eq!(
|
||||
display,
|
||||
concat!(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::Deref;
|
||||
|
||||
use itertools::Either;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::{Class, KnownClass, Type};
|
||||
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type};
|
||||
use crate::Db;
|
||||
|
||||
/// The inferred method resolution order of a given class.
|
||||
@@ -117,7 +117,7 @@ impl<'db> Mro<'db> {
|
||||
for (index, base) in valid_bases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, base)| Some((index, base.into_class()?)))
|
||||
.filter_map(|(index, base)| Some((index, base.into_class_literal_type()?)))
|
||||
{
|
||||
if !seen_bases.insert(base) {
|
||||
duplicate_bases.push((index, base));
|
||||
@@ -287,6 +287,136 @@ pub(super) enum MroErrorKind<'db> {
|
||||
UnresolvableMro { bases_list: Box<[ClassBase<'db>]> },
|
||||
}
|
||||
|
||||
/// Enumeration of the possible kinds of types we allow in class bases.
|
||||
///
|
||||
/// This is much more limited than the [`Type`] enum:
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::Unknown`]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub enum ClassBase<'db> {
|
||||
Any,
|
||||
Unknown,
|
||||
Todo(TodoType),
|
||||
Class(Class<'db>),
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
struct Display<'db> {
|
||||
base: ClassBase<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Any => f.write_str("Any"),
|
||||
ClassBase::Todo(todo) => todo.fmt(f),
|
||||
ClassBase::Unknown => f.write_str("Unknown"),
|
||||
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Display { base: self, db }
|
||||
}
|
||||
|
||||
/// Return a `ClassBase` representing the class `builtins.object`
|
||||
fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.into_class_literal()
|
||||
.map_or(Self::Unknown, |ClassLiteralType { class }| {
|
||||
Self::Class(class)
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to resolve `ty` into a `ClassBase`.
|
||||
///
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
Type::Todo(todo) => Some(Self::Todo(todo)),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SubclassOf(_) => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::TypeVar(_)
|
||||
| KnownInstanceType::TypeAliasType(_)
|
||||
| KnownInstanceType::Literal
|
||||
| KnownInstanceType::LiteralString
|
||||
| KnownInstanceType::Union
|
||||
| KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Optional => None,
|
||||
KnownInstanceType::Any => Some(Self::Any),
|
||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||
KnownInstanceType::Type => {
|
||||
ClassBase::try_from_ty(db, KnownClass::Type.to_class_literal(db))
|
||||
}
|
||||
KnownInstanceType::Tuple => {
|
||||
ClassBase::try_from_ty(db, KnownClass::Tuple.to_class_literal(db))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_class_literal_type(self) -> Option<Class<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the MRO of this base
|
||||
fn mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
|
||||
match self {
|
||||
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Unknown => {
|
||||
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Todo(todo) => {
|
||||
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Class<'db>> for ClassBase<'db> {
|
||||
fn from(value: Class<'db>) -> Self {
|
||||
ClassBase::Class(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
ClassBase::Any => Type::Any,
|
||||
ClassBase::Todo(todo) => Type::Todo(todo),
|
||||
ClassBase::Unknown => Type::Unknown,
|
||||
ClassBase::Class(class) => Type::class_literal(class),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of the [C3-merge algorithm] for calculating a Python class's
|
||||
/// [method resolution order].
|
||||
///
|
||||
|
||||
@@ -196,7 +196,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
is_positive: bool,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
match expression_node {
|
||||
ast::Expr::Name(name) => Some(self.evaluate_expr_name(name, is_positive)),
|
||||
ast::Expr::Compare(expr_compare) => {
|
||||
self.evaluate_expr_compare(expr_compare, expression, is_positive)
|
||||
}
|
||||
@@ -255,31 +254,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_expr_name(
|
||||
&mut self,
|
||||
expr_name: &ast::ExprName,
|
||||
is_positive: bool,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
let ast::ExprName { id, .. } = expr_name;
|
||||
|
||||
let symbol = self
|
||||
.symbols()
|
||||
.symbol_id_by_name(id)
|
||||
.expect("Should always have a symbol for every Name node");
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
|
||||
constraints.insert(
|
||||
symbol,
|
||||
if is_positive {
|
||||
Type::AlwaysFalsy.negate(self.db)
|
||||
} else {
|
||||
Type::AlwaysTruthy.negate(self.db)
|
||||
},
|
||||
);
|
||||
|
||||
constraints
|
||||
}
|
||||
|
||||
fn evaluate_expr_compare(
|
||||
&mut self,
|
||||
expr_compare: &ast::ExprCompare,
|
||||
|
||||
@@ -65,18 +65,9 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
||||
Ty::BuiltinClassLiteral("bool"),
|
||||
Ty::BuiltinClassLiteral("object"),
|
||||
Ty::BuiltinInstance("type"),
|
||||
Ty::AbcInstance("ABC"),
|
||||
Ty::AbcInstance("ABCMeta"),
|
||||
Ty::SubclassOfAny,
|
||||
Ty::SubclassOfBuiltinClass("object"),
|
||||
Ty::SubclassOfBuiltinClass("str"),
|
||||
Ty::SubclassOfBuiltinClass("type"),
|
||||
Ty::AbcClassLiteral("ABC"),
|
||||
Ty::AbcClassLiteral("ABCMeta"),
|
||||
Ty::SubclassOfAbcClass("ABC"),
|
||||
Ty::SubclassOfAbcClass("ABCMeta"),
|
||||
Ty::AlwaysTruthy,
|
||||
Ty::AlwaysFalsy,
|
||||
])
|
||||
.unwrap()
|
||||
.clone()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_python_ast::str::raw_contents;
|
||||
use ruff_python_ast::{self as ast, ModExpression, StringFlags};
|
||||
use ruff_python_parser::{parse_expression_range, Parsed};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::declare_lint;
|
||||
use crate::lint::{Level, LintStatus};
|
||||
|
||||
use super::context::InferContext;
|
||||
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::{declare_lint, Db};
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
@@ -127,23 +127,24 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
|
||||
|
||||
/// Parses the given expression as a string annotation.
|
||||
pub(crate) fn parse_string_annotation(
|
||||
context: &InferContext,
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
string_expr: &ast::ExprStringLiteral,
|
||||
) -> Option<Parsed<ModExpression>> {
|
||||
let file = context.file();
|
||||
let db = context.db();
|
||||
|
||||
) -> AnnotationParseResult {
|
||||
let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), file=%file.path(db)).entered();
|
||||
|
||||
let source = source_text(db.upcast(), file);
|
||||
let node_text = &source[string_expr.range()];
|
||||
let mut diagnostics = TypeCheckDiagnosticsBuilder::new(db, file);
|
||||
|
||||
if let [string_literal] = string_expr.value.as_slice() {
|
||||
let prefix = string_literal.flags.prefix();
|
||||
if prefix.is_raw() {
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&RAW_STRING_TYPE_ANNOTATION,
|
||||
string_literal.into(),
|
||||
format_args!("Type expressions cannot use raw string literal"),
|
||||
@@ -166,8 +167,8 @@ pub(crate) fn parse_string_annotation(
|
||||
// """ = 1
|
||||
// ```
|
||||
match parse_expression_range(source.as_str(), range_excluding_quotes) {
|
||||
Ok(parsed) => return Some(parsed),
|
||||
Err(parse_error) => context.report_lint(
|
||||
Ok(parsed) => return Ok(parsed),
|
||||
Err(parse_error) => diagnostics.add_lint(
|
||||
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
string_literal.into(),
|
||||
format_args!("Syntax error in forward annotation: {}", parse_error.error),
|
||||
@@ -176,7 +177,7 @@ pub(crate) fn parse_string_annotation(
|
||||
} else {
|
||||
// The raw contents of the string doesn't match the parsed content. This could be the
|
||||
// case for annotations that contain escape sequences.
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
|
||||
string_expr.into(),
|
||||
format_args!("Type expressions cannot contain escape characters"),
|
||||
@@ -184,12 +185,12 @@ pub(crate) fn parse_string_annotation(
|
||||
}
|
||||
} else {
|
||||
// String is implicitly concatenated.
|
||||
context.report_lint(
|
||||
diagnostics.add_lint(
|
||||
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
|
||||
string_expr.into(),
|
||||
format_args!("Type expressions cannot span multiple string literals"),
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
Err(diagnostics.finish())
|
||||
}
|
||||
|
||||
@@ -6,34 +6,30 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{todo_type, Type, TypeCheckDiagnostics};
|
||||
use crate::types::{todo_type, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::Db;
|
||||
|
||||
use super::context::{InferContext, WithDiagnostics};
|
||||
|
||||
/// Unpacks the value expression type to their respective targets.
|
||||
pub(crate) struct Unpacker<'db> {
|
||||
context: InferContext<'db>,
|
||||
db: &'db dyn Db,
|
||||
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
|
||||
diagnostics: TypeCheckDiagnosticsBuilder<'db>,
|
||||
}
|
||||
|
||||
impl<'db> Unpacker<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
Self {
|
||||
context: InferContext::new(db, file),
|
||||
db,
|
||||
targets: FxHashMap::default(),
|
||||
diagnostics: TypeCheckDiagnosticsBuilder::new(db, file),
|
||||
}
|
||||
}
|
||||
|
||||
fn db(&self) -> &'db dyn Db {
|
||||
self.context.db()
|
||||
}
|
||||
|
||||
pub(crate) fn unpack(&mut self, target: &ast::Expr, value_ty: Type<'db>, scope: ScopeId<'db>) {
|
||||
match target {
|
||||
ast::Expr::Name(target_name) => {
|
||||
self.targets
|
||||
.insert(target_name.scoped_expression_id(self.db(), scope), value_ty);
|
||||
.insert(target_name.scoped_expression_id(self.db, scope), value_ty);
|
||||
}
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
self.unpack(value, value_ty, scope);
|
||||
@@ -44,11 +40,11 @@ impl<'db> Unpacker<'db> {
|
||||
let starred_index = elts.iter().position(ast::Expr::is_starred_expr);
|
||||
|
||||
let element_types = if let Some(starred_index) = starred_index {
|
||||
if tuple_ty.len(self.db()) >= elts.len() - 1 {
|
||||
if tuple_ty.len(self.db) >= elts.len() - 1 {
|
||||
let mut element_types = Vec::with_capacity(elts.len());
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db())[..starred_index],
|
||||
&tuple_ty.elements(self.db)[..starred_index],
|
||||
);
|
||||
|
||||
// E.g., in `(a, *b, c, d) = ...`, the index of starred element `b`
|
||||
@@ -56,10 +52,10 @@ impl<'db> Unpacker<'db> {
|
||||
let remaining = elts.len() - (starred_index + 1);
|
||||
// This index represents the type of the last element that belongs
|
||||
// to the starred expression, in an exclusive manner.
|
||||
let starred_end_index = tuple_ty.len(self.db()) - remaining;
|
||||
let starred_end_index = tuple_ty.len(self.db) - remaining;
|
||||
// SAFETY: Safe because of the length check above.
|
||||
let _starred_element_types =
|
||||
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
|
||||
&tuple_ty.elements(self.db)[starred_index..starred_end_index];
|
||||
// TODO: Combine the types into a list type. If the
|
||||
// starred_element_types is empty, then it should be `List[Any]`.
|
||||
// combine_types(starred_element_types);
|
||||
@@ -67,11 +63,11 @@ impl<'db> Unpacker<'db> {
|
||||
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db())[starred_end_index..],
|
||||
&tuple_ty.elements(self.db)[starred_end_index..],
|
||||
);
|
||||
Cow::Owned(element_types)
|
||||
} else {
|
||||
let mut element_types = tuple_ty.elements(self.db()).to_vec();
|
||||
let mut element_types = tuple_ty.elements(self.db).to_vec();
|
||||
// Subtract 1 to insert the starred expression type at the correct
|
||||
// index.
|
||||
element_types.resize(elts.len() - 1, Type::Unknown);
|
||||
@@ -80,7 +76,7 @@ impl<'db> Unpacker<'db> {
|
||||
Cow::Owned(element_types)
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(tuple_ty.elements(self.db()).as_ref())
|
||||
Cow::Borrowed(tuple_ty.elements(self.db).as_ref())
|
||||
};
|
||||
|
||||
for (index, element) in elts.iter().enumerate() {
|
||||
@@ -98,9 +94,9 @@ impl<'db> Unpacker<'db> {
|
||||
// individual character, instead of just an array of `LiteralString`, but
|
||||
// there would be a cost and it's not clear that it's worth it.
|
||||
let value_ty = Type::tuple(
|
||||
self.db(),
|
||||
self.db,
|
||||
std::iter::repeat(Type::LiteralString)
|
||||
.take(string_literal_ty.python_len(self.db())),
|
||||
.take(string_literal_ty.python_len(self.db)),
|
||||
);
|
||||
self.unpack(target, value_ty, scope);
|
||||
}
|
||||
@@ -109,8 +105,8 @@ impl<'db> Unpacker<'db> {
|
||||
Type::LiteralString
|
||||
} else {
|
||||
value_ty
|
||||
.iterate(self.db())
|
||||
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
|
||||
.iterate(self.db)
|
||||
.unwrap_with_diagnostic(AnyNodeRef::from(target), &mut self.diagnostics)
|
||||
};
|
||||
for element in elts {
|
||||
self.unpack(element, value_ty, scope);
|
||||
@@ -124,7 +120,7 @@ impl<'db> Unpacker<'db> {
|
||||
pub(crate) fn finish(mut self) -> UnpackResult<'db> {
|
||||
self.targets.shrink_to_fit();
|
||||
UnpackResult {
|
||||
diagnostics: self.context.finish(),
|
||||
diagnostics: self.diagnostics.finish(),
|
||||
targets: self.targets,
|
||||
}
|
||||
}
|
||||
@@ -140,10 +136,8 @@ impl<'db> UnpackResult<'db> {
|
||||
pub(crate) fn get(&self, expr_id: ScopedExpressionId) -> Option<Type<'db>> {
|
||||
self.targets.get(&expr_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl WithDiagnostics for UnpackResult<'_> {
|
||||
fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +91,11 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
|
||||
let db = match path {
|
||||
AnySystemPath::System(path) => {
|
||||
match session.workspace_db_for_path(path.as_std_path()) {
|
||||
Some(db) => db.clone(),
|
||||
None => session.default_workspace_db().clone(),
|
||||
Some(db) => db.snapshot(),
|
||||
None => session.default_workspace_db().snapshot(),
|
||||
}
|
||||
}
|
||||
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().clone(),
|
||||
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().snapshot(),
|
||||
};
|
||||
|
||||
let Some(snapshot) = session.take_snapshot(url) else {
|
||||
|
||||
@@ -11,9 +11,9 @@ authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true, features = ["serde"] }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
@@ -30,5 +30,7 @@ smallvec = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -234,15 +234,13 @@ language tag:
|
||||
````markdown
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
target-version = "3.10"
|
||||
```
|
||||
````
|
||||
|
||||
This configuration will apply to all tests in the same section, and all nested sections within that
|
||||
section. Nested sections can override configurations from their parent sections.
|
||||
|
||||
See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_test/src/config.rs) for the full list of supported configuration options.
|
||||
|
||||
## Documentation of tests
|
||||
|
||||
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
|
||||
|
||||
@@ -3,45 +3,26 @@
|
||||
//! following limited structure:
|
||||
//!
|
||||
//! ```toml
|
||||
//! log = true # or log = "red_knot=WARN"
|
||||
//! [environment]
|
||||
//! python-version = "3.10"
|
||||
//! target-version = "3.10"
|
||||
//! ```
|
||||
|
||||
use anyhow::Context;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct MarkdownTestConfig {
|
||||
pub(crate) environment: Option<Environment>,
|
||||
|
||||
pub(crate) log: Option<Log>,
|
||||
pub(crate) environment: Environment,
|
||||
}
|
||||
|
||||
impl MarkdownTestConfig {
|
||||
pub(crate) fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
toml::from_str(s).context("Error while parsing Markdown TOML config")
|
||||
}
|
||||
|
||||
pub(crate) fn python_version(&self) -> Option<PythonVersion> {
|
||||
self.environment.as_ref().and_then(|env| env.python_version)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct Environment {
|
||||
/// Python version to assume when resolving types.
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Log {
|
||||
/// Enable logging with tracing when `true`.
|
||||
Bool(bool),
|
||||
/// Enable logging and only show filters that match the given [env-filter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
|
||||
Filter(String),
|
||||
#[serde(rename = "target-version")]
|
||||
pub(crate) target_version: String,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Db {
|
||||
workspace_root: SystemPathBuf,
|
||||
storage: salsa::Storage<Self>,
|
||||
@@ -39,7 +38,7 @@ impl Db {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(db.workspace_root.clone()),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::config::Log;
|
||||
use camino::Utf8Path;
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
@@ -8,7 +7,6 @@ use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_text_size::TextSize;
|
||||
use salsa::Setter;
|
||||
@@ -44,14 +42,9 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
continue;
|
||||
}
|
||||
|
||||
let _tracing = test.configuration().log.as_ref().and_then(|log| match log {
|
||||
Log::Bool(enabled) => enabled.then(setup_logging),
|
||||
Log::Filter(filter) => setup_logging_with_filter(filter),
|
||||
});
|
||||
|
||||
Program::get(&db)
|
||||
.set_python_version(&mut db)
|
||||
.to(test.configuration().python_version().unwrap_or_default());
|
||||
.set_target_version(&mut db)
|
||||
.to(test.target_version());
|
||||
|
||||
// Remove all files so that the db is in a "fresh" state.
|
||||
db.memory_file_system().remove_all();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::{bail, Context};
|
||||
use memchr::memchr2;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use regex::{Captures, Match, Regex};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_trivia::Cursor;
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
use crate::config::MarkdownTestConfig;
|
||||
|
||||
@@ -74,8 +74,8 @@ impl<'m, 's> MarkdownTest<'m, 's> {
|
||||
self.files.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn configuration(&self) -> &MarkdownTestConfig {
|
||||
&self.section.config
|
||||
pub(crate) fn target_version(&self) -> PythonVersion {
|
||||
self.section.target_version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ struct Section<'s> {
|
||||
title: &'s str,
|
||||
level: u8,
|
||||
parent_id: Option<SectionId>,
|
||||
config: MarkdownTestConfig,
|
||||
target_version: PythonVersion,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
@@ -157,14 +157,8 @@ static HEADER_RE: LazyLock<Regex> =
|
||||
/// Matches a code block fenced by triple backticks, possibly with language and `key=val`
|
||||
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
||||
static CODE_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?x)
|
||||
^```(?<lang>(?-u:\w)+)?(?<config>(?:\x20+\S+)*)\s*\n
|
||||
(?<code>(?:.|\n)*?)\n?
|
||||
(?<end>```|\z)
|
||||
",
|
||||
)
|
||||
.unwrap()
|
||||
Regex::new(r"^```(?<lang>(?-u:\w)+)?(?<config>(?: +\S+)*)\s*\n(?<code>(?:.|\n)*?)\n?```\s*\n?")
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -209,7 +203,6 @@ struct Parser<'s> {
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
cursor: Cursor<'s>,
|
||||
|
||||
source: &'s str,
|
||||
source_len: TextSize,
|
||||
|
||||
/// Stack of ancestor sections.
|
||||
@@ -229,11 +222,10 @@ impl<'s> Parser<'s> {
|
||||
title,
|
||||
level: 0,
|
||||
parent_id: None,
|
||||
config: MarkdownTestConfig::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
});
|
||||
Self {
|
||||
sections,
|
||||
source,
|
||||
files: IndexVec::default(),
|
||||
cursor: Cursor::new(source),
|
||||
source_len: source.text_len(),
|
||||
@@ -313,7 +305,7 @@ impl<'s> Parser<'s> {
|
||||
title,
|
||||
level: header_level.try_into()?,
|
||||
parent_id: Some(parent),
|
||||
config: self.sections[parent].config.clone(),
|
||||
target_version: self.sections[parent].target_version,
|
||||
};
|
||||
|
||||
if self.current_section_files.is_some() {
|
||||
@@ -337,13 +329,6 @@ impl<'s> Parser<'s> {
|
||||
// We never pop the implicit root section.
|
||||
let section = self.stack.top();
|
||||
|
||||
if captures.name("end").unwrap().is_empty() {
|
||||
let code_block_start = self.cursor.token_len();
|
||||
let line = self.source.count_lines(TextRange::up_to(code_block_start)) + 1;
|
||||
|
||||
return Err(anyhow::anyhow!("Unterminated code block at line {line}."));
|
||||
}
|
||||
|
||||
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
|
||||
|
||||
if let Some(config_match) = captures.name("config") {
|
||||
@@ -413,8 +398,23 @@ impl<'s> Parser<'s> {
|
||||
bail!("Multiple TOML configuration blocks in the same section are not allowed.");
|
||||
}
|
||||
|
||||
let config = MarkdownTestConfig::from_str(code)?;
|
||||
let target_version = config.environment.target_version;
|
||||
|
||||
let parts = target_version
|
||||
.split('.')
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context(format!(
|
||||
"Invalid 'target-version' component: '{target_version}'"
|
||||
))?;
|
||||
|
||||
if parts.len() != 2 {
|
||||
bail!("Invalid 'target-version': expected MAJOR.MINOR, got '{target_version}'.",);
|
||||
}
|
||||
|
||||
let current_section = &mut self.sections[self.stack.top()];
|
||||
current_section.config = MarkdownTestConfig::from_str(code)?;
|
||||
current_section.target_version = PythonVersion::from((parts[0], parts[1]));
|
||||
|
||||
self.current_section_has_config = true;
|
||||
|
||||
@@ -680,38 +680,6 @@ mod tests {
|
||||
assert_eq!(file.code, "x = 10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unterminated_code_block_1() {
|
||||
let source = dedent(
|
||||
"
|
||||
```
|
||||
x = 1
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Unterminated code block at line 2.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unterminated_code_block_2() {
|
||||
let source = dedent(
|
||||
"
|
||||
## A well-fenced block
|
||||
|
||||
```
|
||||
y = 2
|
||||
```
|
||||
|
||||
## A not-so-well-fenced block
|
||||
|
||||
```
|
||||
x = 1
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Unterminated code block at line 10.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_header_inside_test() {
|
||||
let source = dedent(
|
||||
|
||||
@@ -1 +1 @@
|
||||
fc11e835108394728930059c8db5b436209bc957
|
||||
0a2da01946a406ede42e9c66f416a7e7758991d6
|
||||
|
||||
@@ -57,7 +57,6 @@ _msi: 3.0-3.12
|
||||
_multibytecodec: 3.0-
|
||||
_operator: 3.4-
|
||||
_osx_support: 3.0-
|
||||
_pickle: 3.0-
|
||||
_posixsubprocess: 3.2-
|
||||
_py_abc: 3.7-
|
||||
_pydecimal: 3.5-
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
from _threading_local import local as local
|
||||
import sys
|
||||
from _thread import _excepthook, _ExceptHookArgs
|
||||
from _typeshed import ProfileFunction, TraceFunction
|
||||
from threading import (
|
||||
TIMEOUT_MAX as TIMEOUT_MAX,
|
||||
Barrier as Barrier,
|
||||
BoundedSemaphore as BoundedSemaphore,
|
||||
BrokenBarrierError as BrokenBarrierError,
|
||||
Condition as Condition,
|
||||
Event as Event,
|
||||
ExceptHookArgs as ExceptHookArgs,
|
||||
Lock as Lock,
|
||||
RLock as RLock,
|
||||
Semaphore as Semaphore,
|
||||
Thread as Thread,
|
||||
ThreadError as ThreadError,
|
||||
Timer as Timer,
|
||||
_DummyThread as _DummyThread,
|
||||
_RLock as _RLock,
|
||||
excepthook as excepthook,
|
||||
)
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from types import TracebackType
|
||||
from typing import Any, TypeVar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
__all__ = [
|
||||
"get_ident",
|
||||
@@ -54,3 +42,123 @@ def main_thread() -> Thread: ...
|
||||
def settrace(func: TraceFunction) -> None: ...
|
||||
def setprofile(func: ProfileFunction | None) -> None: ...
|
||||
def stack_size(size: int | None = None) -> int: ...
|
||||
|
||||
TIMEOUT_MAX: float
|
||||
|
||||
class ThreadError(Exception): ...
|
||||
|
||||
class local:
|
||||
def __getattribute__(self, name: str) -> Any: ...
|
||||
def __setattr__(self, name: str, value: Any) -> None: ...
|
||||
def __delattr__(self, name: str) -> None: ...
|
||||
|
||||
class Thread:
|
||||
name: str
|
||||
daemon: bool
|
||||
@property
|
||||
def ident(self) -> int | None: ...
|
||||
def __init__(
|
||||
self,
|
||||
group: None = None,
|
||||
target: Callable[..., object] | None = None,
|
||||
name: str | None = None,
|
||||
args: Iterable[Any] = (),
|
||||
kwargs: Mapping[str, Any] | None = None,
|
||||
*,
|
||||
daemon: bool | None = None,
|
||||
) -> None: ...
|
||||
def start(self) -> None: ...
|
||||
def run(self) -> None: ...
|
||||
def join(self, timeout: float | None = None) -> None: ...
|
||||
def getName(self) -> str: ...
|
||||
def setName(self, name: str) -> None: ...
|
||||
@property
|
||||
def native_id(self) -> int | None: ... # only available on some platforms
|
||||
def is_alive(self) -> bool: ...
|
||||
if sys.version_info < (3, 9):
|
||||
def isAlive(self) -> bool: ...
|
||||
|
||||
def isDaemon(self) -> bool: ...
|
||||
def setDaemon(self, daemonic: bool) -> None: ...
|
||||
|
||||
class _DummyThread(Thread): ...
|
||||
|
||||
class Lock:
|
||||
def __enter__(self) -> bool: ...
|
||||
def __exit__(
|
||||
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
|
||||
) -> bool | None: ...
|
||||
def acquire(self, blocking: bool = ..., timeout: float = ...) -> bool: ...
|
||||
def release(self) -> None: ...
|
||||
def locked(self) -> bool: ...
|
||||
|
||||
class _RLock:
|
||||
def __enter__(self) -> bool: ...
|
||||
def __exit__(
|
||||
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
|
||||
) -> bool | None: ...
|
||||
def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: ...
|
||||
def release(self) -> None: ...
|
||||
|
||||
RLock = _RLock
|
||||
|
||||
class Condition:
|
||||
def __init__(self, lock: Lock | _RLock | None = None) -> None: ...
|
||||
def __enter__(self) -> bool: ...
|
||||
def __exit__(
|
||||
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
|
||||
) -> bool | None: ...
|
||||
def acquire(self, blocking: bool = ..., timeout: float = ...) -> bool: ...
|
||||
def release(self) -> None: ...
|
||||
def wait(self, timeout: float | None = None) -> bool: ...
|
||||
def wait_for(self, predicate: Callable[[], _T], timeout: float | None = None) -> _T: ...
|
||||
def notify(self, n: int = 1) -> None: ...
|
||||
def notify_all(self) -> None: ...
|
||||
def notifyAll(self) -> None: ...
|
||||
|
||||
class Semaphore:
|
||||
def __init__(self, value: int = 1) -> None: ...
|
||||
def __exit__(
|
||||
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
|
||||
) -> bool | None: ...
|
||||
def acquire(self, blocking: bool = True, timeout: float | None = None) -> bool: ...
|
||||
def __enter__(self, blocking: bool = True, timeout: float | None = None) -> bool: ...
|
||||
if sys.version_info >= (3, 9):
|
||||
def release(self, n: int = ...) -> None: ...
|
||||
else:
|
||||
def release(self) -> None: ...
|
||||
|
||||
class BoundedSemaphore(Semaphore): ...
|
||||
|
||||
class Event:
|
||||
def is_set(self) -> bool: ...
|
||||
def set(self) -> None: ...
|
||||
def clear(self) -> None: ...
|
||||
def wait(self, timeout: float | None = None) -> bool: ...
|
||||
|
||||
excepthook = _excepthook
|
||||
ExceptHookArgs = _ExceptHookArgs
|
||||
|
||||
class Timer(Thread):
|
||||
def __init__(
|
||||
self,
|
||||
interval: float,
|
||||
function: Callable[..., object],
|
||||
args: Iterable[Any] | None = None,
|
||||
kwargs: Mapping[str, Any] | None = None,
|
||||
) -> None: ...
|
||||
def cancel(self) -> None: ...
|
||||
|
||||
class Barrier:
|
||||
@property
|
||||
def parties(self) -> int: ...
|
||||
@property
|
||||
def n_waiting(self) -> int: ...
|
||||
@property
|
||||
def broken(self) -> bool: ...
|
||||
def __init__(self, parties: int, action: Callable[[], None] | None = None, timeout: float | None = None) -> None: ...
|
||||
def wait(self, timeout: float | None = None) -> int: ...
|
||||
def reset(self) -> None: ...
|
||||
def abort(self) -> None: ...
|
||||
|
||||
class BrokenBarrierError(RuntimeError): ...
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import sys
|
||||
from _typeshed import ReadableBuffer, SupportsWrite
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||
from pickle import PickleBuffer as PickleBuffer
|
||||
from typing import Any, Protocol, type_check_only
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
class _ReadableFileobj(Protocol):
|
||||
def read(self, n: int, /) -> bytes: ...
|
||||
def readline(self) -> bytes: ...
|
||||
|
||||
_BufferCallback: TypeAlias = Callable[[PickleBuffer], Any] | None
|
||||
|
||||
_ReducedType: TypeAlias = (
|
||||
str
|
||||
| tuple[Callable[..., Any], tuple[Any, ...]]
|
||||
| tuple[Callable[..., Any], tuple[Any, ...], Any]
|
||||
| tuple[Callable[..., Any], tuple[Any, ...], Any, Iterator[Any] | None]
|
||||
| tuple[Callable[..., Any], tuple[Any, ...], Any, Iterator[Any] | None, Iterator[Any] | None]
|
||||
)
|
||||
|
||||
def dump(
|
||||
obj: Any,
|
||||
file: SupportsWrite[bytes],
|
||||
protocol: int | None = None,
|
||||
*,
|
||||
fix_imports: bool = True,
|
||||
buffer_callback: _BufferCallback = None,
|
||||
) -> None: ...
|
||||
def dumps(
|
||||
obj: Any, protocol: int | None = None, *, fix_imports: bool = True, buffer_callback: _BufferCallback = None
|
||||
) -> bytes: ...
|
||||
def load(
|
||||
file: _ReadableFileobj,
|
||||
*,
|
||||
fix_imports: bool = True,
|
||||
encoding: str = "ASCII",
|
||||
errors: str = "strict",
|
||||
buffers: Iterable[Any] | None = (),
|
||||
) -> Any: ...
|
||||
def loads(
|
||||
data: ReadableBuffer,
|
||||
/,
|
||||
*,
|
||||
fix_imports: bool = True,
|
||||
encoding: str = "ASCII",
|
||||
errors: str = "strict",
|
||||
buffers: Iterable[Any] | None = (),
|
||||
) -> Any: ...
|
||||
|
||||
class PickleError(Exception): ...
|
||||
class PicklingError(PickleError): ...
|
||||
class UnpicklingError(PickleError): ...
|
||||
|
||||
@type_check_only
|
||||
class PicklerMemoProxy:
|
||||
def clear(self, /) -> None: ...
|
||||
def copy(self, /) -> dict[int, tuple[int, Any]]: ...
|
||||
|
||||
class Pickler:
|
||||
fast: bool
|
||||
dispatch_table: Mapping[type, Callable[[Any], _ReducedType]]
|
||||
reducer_override: Callable[[Any], Any]
|
||||
bin: bool # undocumented
|
||||
def __init__(
|
||||
self,
|
||||
file: SupportsWrite[bytes],
|
||||
protocol: int | None = None,
|
||||
*,
|
||||
fix_imports: bool = True,
|
||||
buffer_callback: _BufferCallback = None,
|
||||
) -> None: ...
|
||||
@property
|
||||
def memo(self) -> PicklerMemoProxy: ...
|
||||
@memo.setter
|
||||
def memo(self, value: PicklerMemoProxy | dict[int, tuple[int, Any]]) -> None: ...
|
||||
def dump(self, obj: Any, /) -> None: ...
|
||||
def clear_memo(self) -> None: ...
|
||||
if sys.version_info >= (3, 13):
|
||||
def persistent_id(self, obj: Any, /) -> Any: ...
|
||||
else:
|
||||
persistent_id: Callable[[Any], Any]
|
||||
|
||||
@type_check_only
|
||||
class UnpicklerMemoProxy:
|
||||
def clear(self, /) -> None: ...
|
||||
def copy(self, /) -> dict[int, tuple[int, Any]]: ...
|
||||
|
||||
class Unpickler:
|
||||
def __init__(
|
||||
self,
|
||||
file: _ReadableFileobj,
|
||||
*,
|
||||
fix_imports: bool = True,
|
||||
encoding: str = "ASCII",
|
||||
errors: str = "strict",
|
||||
buffers: Iterable[Any] | None = (),
|
||||
) -> None: ...
|
||||
@property
|
||||
def memo(self) -> UnpicklerMemoProxy: ...
|
||||
@memo.setter
|
||||
def memo(self, value: UnpicklerMemoProxy | dict[int, tuple[int, Any]]) -> None: ...
|
||||
def load(self) -> Any: ...
|
||||
def find_class(self, module_name: str, global_name: str, /) -> Any: ...
|
||||
if sys.version_info >= (3, 13):
|
||||
def persistent_load(self, pid: Any, /) -> Any: ...
|
||||
else:
|
||||
persistent_load: Callable[[Any], Any]
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from typing import ClassVar, Literal, NoReturn
|
||||
|
||||
@@ -6,7 +5,7 @@ class Quitter:
|
||||
name: str
|
||||
eof: str
|
||||
def __init__(self, name: str, eof: str) -> None: ...
|
||||
def __call__(self, code: sys._ExitCode = None) -> NoReturn: ...
|
||||
def __call__(self, code: int | None = None) -> NoReturn: ...
|
||||
|
||||
class _Printer:
|
||||
MAXLINES: ClassVar[Literal[23]]
|
||||
@@ -14,4 +13,4 @@ class _Printer:
|
||||
def __call__(self) -> None: ...
|
||||
|
||||
class _Helper:
|
||||
def __call__(self, request: object = ...) -> None: ...
|
||||
def __call__(self, request: object) -> None: ...
|
||||
|
||||
@@ -78,10 +78,8 @@ if sys.platform == "win32":
|
||||
SO_EXCLUSIVEADDRUSE: int
|
||||
if sys.platform != "win32":
|
||||
SO_REUSEPORT: int
|
||||
if sys.platform != "darwin" or sys.version_info >= (3, 13):
|
||||
SO_BINDTODEVICE: int
|
||||
|
||||
if sys.platform != "win32" and sys.platform != "darwin":
|
||||
SO_BINDTODEVICE: int
|
||||
SO_DOMAIN: int
|
||||
SO_MARK: int
|
||||
SO_PASSCRED: int
|
||||
|
||||
@@ -113,31 +113,16 @@ TK_VERSION: Final[str]
|
||||
class TkttType:
|
||||
def deletetimerhandler(self): ...
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
def create(
|
||||
screenName: str | None = None,
|
||||
baseName: str = "",
|
||||
className: str = "Tk",
|
||||
interactive: bool = False,
|
||||
wantobjects: int = 0,
|
||||
wantTk: bool = True,
|
||||
sync: bool = False,
|
||||
use: str | None = None,
|
||||
/,
|
||||
): ...
|
||||
|
||||
else:
|
||||
def create(
|
||||
screenName: str | None = None,
|
||||
baseName: str = "",
|
||||
className: str = "Tk",
|
||||
interactive: bool = False,
|
||||
wantobjects: bool = False,
|
||||
wantTk: bool = True,
|
||||
sync: bool = False,
|
||||
use: str | None = None,
|
||||
/,
|
||||
): ...
|
||||
|
||||
def create(
|
||||
screenName: str | None = None,
|
||||
baseName: str = "",
|
||||
className: str = "Tk",
|
||||
interactive: bool = False,
|
||||
wantobjects: bool = False,
|
||||
wantTk: bool = True,
|
||||
sync: bool = False,
|
||||
use: str | None = None,
|
||||
/,
|
||||
): ...
|
||||
def getbusywaitinterval(): ...
|
||||
def setbusywaitinterval(new_val, /): ...
|
||||
|
||||
@@ -182,30 +182,30 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
def add_subparsers(
|
||||
self: _ArgumentParserT,
|
||||
*,
|
||||
title: str = "subcommands",
|
||||
description: str | None = None,
|
||||
prog: str | None = None,
|
||||
title: str = ...,
|
||||
description: str | None = ...,
|
||||
prog: str = ...,
|
||||
action: type[Action] = ...,
|
||||
option_string: str = ...,
|
||||
dest: str | None = None,
|
||||
required: bool = False,
|
||||
help: str | None = None,
|
||||
metavar: str | None = None,
|
||||
dest: str | None = ...,
|
||||
required: bool = ...,
|
||||
help: str | None = ...,
|
||||
metavar: str | None = ...,
|
||||
) -> _SubParsersAction[_ArgumentParserT]: ...
|
||||
@overload
|
||||
def add_subparsers(
|
||||
self,
|
||||
*,
|
||||
title: str = "subcommands",
|
||||
description: str | None = None,
|
||||
prog: str | None = None,
|
||||
title: str = ...,
|
||||
description: str | None = ...,
|
||||
prog: str = ...,
|
||||
parser_class: type[_ArgumentParserT],
|
||||
action: type[Action] = ...,
|
||||
option_string: str = ...,
|
||||
dest: str | None = None,
|
||||
required: bool = False,
|
||||
help: str | None = None,
|
||||
metavar: str | None = None,
|
||||
dest: str | None = ...,
|
||||
required: bool = ...,
|
||||
help: str | None = ...,
|
||||
metavar: str | None = ...,
|
||||
) -> _SubParsersAction[_ArgumentParserT]: ...
|
||||
def print_usage(self, file: IO[str] | None = None) -> None: ...
|
||||
def print_help(self, file: IO[str] | None = None) -> None: ...
|
||||
@@ -237,13 +237,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
# undocumented
|
||||
def _get_optional_actions(self) -> list[Action]: ...
|
||||
def _get_positional_actions(self) -> list[Action]: ...
|
||||
if sys.version_info >= (3, 12):
|
||||
def _parse_known_args(
|
||||
self, arg_strings: list[str], namespace: Namespace, intermixed: bool
|
||||
) -> tuple[Namespace, list[str]]: ...
|
||||
else:
|
||||
def _parse_known_args(self, arg_strings: list[str], namespace: Namespace) -> tuple[Namespace, list[str]]: ...
|
||||
|
||||
def _parse_known_args(self, arg_strings: list[str], namespace: Namespace) -> tuple[Namespace, list[str]]: ...
|
||||
def _read_args_from_files(self, arg_strings: list[str]) -> list[str]: ...
|
||||
def _match_argument(self, action: Action, arg_strings_pattern: str) -> int: ...
|
||||
def _match_arguments_partial(self, actions: Sequence[Action], arg_strings_pattern: str) -> list[int]: ...
|
||||
|
||||
@@ -62,4 +62,3 @@ class _ProactorSocketTransport(_ProactorReadPipeTransport, _ProactorBaseWritePip
|
||||
|
||||
class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
def __init__(self, proactor: Any) -> None: ...
|
||||
async def sock_recv(self, sock: socket, n: int) -> bytes: ...
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import selectors
|
||||
from socket import socket
|
||||
|
||||
from . import base_events
|
||||
|
||||
@@ -7,4 +6,3 @@ __all__ = ("BaseSelectorEventLoop",)
|
||||
|
||||
class BaseSelectorEventLoop(base_events.BaseEventLoop):
|
||||
def __init__(self, selector: selectors.BaseSelector | None = None) -> None: ...
|
||||
async def sock_recv(self, sock: socket, n: int) -> bytes: ...
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user