Compare commits
2 Commits
micha/erro
...
0.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c3b52fd5 | ||
|
|
a388e49f38 |
@@ -17,7 +17,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = 100
|
||||
|
||||
[*.toml]
|
||||
indent_size = 4
|
||||
max_line_length = 100
|
||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -13,10 +13,9 @@
|
||||
# flake8-pyi
|
||||
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood
|
||||
|
||||
# Script for fuzzing the parser/red-knot etc.
|
||||
/python/py-fuzzer/ @AlexWaygood
|
||||
# Script for fuzzing the parser
|
||||
/scripts/fuzz-parser/ @AlexWaygood
|
||||
|
||||
# red-knot
|
||||
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
|
||||
8
.github/workflows/build-binaries.yml
vendored
8
.github/workflows/build-binaries.yml
vendored
@@ -40,7 +40,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -69,7 +68,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -111,7 +109,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -167,7 +164,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -220,7 +216,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -295,7 +290,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -360,7 +354,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -426,7 +419,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
1
.github/workflows/build-docker.yml
vendored
1
.github/workflows/build-docker.yml
vendored
@@ -36,7 +36,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
||||
122
.github/workflows/ci.yaml
vendored
122
.github/workflows/ci.yaml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.12"
|
||||
PYTHON_VERSION: "3.11"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
@@ -32,13 +32,10 @@ jobs:
|
||||
# Flag that is raised when any code is changed
|
||||
# This is superset of the linter and formatter
|
||||
code: ${{ steps.changed.outputs.code_any_changed }}
|
||||
# Flag that is raised when any code that affects the fuzzer is changed
|
||||
fuzz: ${{ steps.changed.outputs.fuzz_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: tj-actions/changed-files@v45
|
||||
id: changed
|
||||
@@ -52,7 +49,7 @@ jobs:
|
||||
- crates/ruff_text_size/**
|
||||
- crates/ruff_python_ast/**
|
||||
- crates/ruff_python_parser/**
|
||||
- python/py-fuzzer/**
|
||||
- scripts/fuzz-parser/**
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
linter:
|
||||
@@ -82,15 +79,9 @@ jobs:
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
fuzz:
|
||||
- fuzz/Cargo.toml
|
||||
- fuzz/Cargo.lock
|
||||
- fuzz/fuzz_targets/**
|
||||
|
||||
code:
|
||||
- "**/*"
|
||||
- "!**/*.md"
|
||||
- "crates/red_knot_python_semantic/resources/mdtest/**/*.md"
|
||||
- "!docs/**"
|
||||
- "!assets/**"
|
||||
|
||||
@@ -100,8 +91,6 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- run: cargo fmt --all --check
|
||||
@@ -114,8 +103,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -128,14 +115,12 @@ jobs:
|
||||
|
||||
cargo-test-linux:
|
||||
name: "cargo test (linux)"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -172,45 +157,14 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
|
||||
cargo-test-linux-release:
|
||||
name: "cargo test (linux, release)"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-test-windows:
|
||||
name: "cargo test (windows)"
|
||||
runs-on: windows-latest-xlarge
|
||||
runs-on: windows-latest
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
@@ -235,8 +189,6 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -245,8 +197,6 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Test ruff_wasm"
|
||||
run: |
|
||||
@@ -260,12 +210,11 @@ jobs:
|
||||
cargo-build-release:
|
||||
name: "cargo build (release)"
|
||||
runs-on: macos-latest
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -282,8 +231,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: SebRollen/toml-action@v1.2.0
|
||||
id: msrv
|
||||
with:
|
||||
@@ -308,16 +255,14 @@ jobs:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-fuzz-build:
|
||||
name: "cargo fuzz build"
|
||||
cargo-fuzz:
|
||||
name: "cargo fuzz"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' }}
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -333,7 +278,7 @@ jobs:
|
||||
- run: cargo fuzz build -s none
|
||||
|
||||
fuzz-parser:
|
||||
name: "fuzz parser"
|
||||
name: "Fuzz the parser"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
@@ -344,9 +289,13 @@ jobs:
|
||||
FORCE_COLOR: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: Install uv
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
- name: Install Python requirements
|
||||
run: uv pip install -r scripts/fuzz-parser/requirements.txt --system
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -358,15 +307,7 @@ jobs:
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
(
|
||||
uvx \
|
||||
--python=${{ env.PYTHON_VERSION }} \
|
||||
--from=./python/py-fuzzer \
|
||||
fuzz \
|
||||
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
|
||||
--bin=ruff \
|
||||
0-500
|
||||
)
|
||||
python scripts/fuzz-parser/fuzz.py 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
scripts:
|
||||
name: "test scripts"
|
||||
@@ -376,8 +317,6 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -392,7 +331,7 @@ jobs:
|
||||
|
||||
ecosystem:
|
||||
name: "ecosystem"
|
||||
runs-on: depot-ubuntu-latest-8
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
@@ -402,8 +341,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -415,7 +352,7 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
- uses: dawidd6/action-download-artifact@v6
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
@@ -514,8 +451,6 @@ jobs:
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
@@ -526,8 +461,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -553,8 +486,6 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -586,8 +517,6 @@ jobs:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
@@ -599,7 +528,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v3
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
@@ -628,18 +557,16 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Cache rust"
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: "Run checks"
|
||||
- name: "Formatter progress"
|
||||
run: scripts/formatter_ecosystem_checks.sh
|
||||
- name: "Github step summary"
|
||||
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
|
||||
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
|
||||
- name: "Remove checkouts from cache"
|
||||
run: rm -r target/formatter-ecosystem
|
||||
run: rm -r target/progress_projects
|
||||
|
||||
check-ruff-lsp:
|
||||
name: "test ruff-lsp"
|
||||
@@ -657,7 +584,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
name: "Download ruff-lsp source"
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
@@ -693,8 +619,6 @@ jobs:
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
19
.github/workflows/daily_fuzz.yaml
vendored
19
.github/workflows/daily_fuzz.yaml
vendored
@@ -32,9 +32,13 @@ jobs:
|
||||
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
python-version: "3.12"
|
||||
- name: Install uv
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
- name: Install Python requirements
|
||||
run: uv pip install -r scripts/fuzz-parser/requirements.txt --system
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -45,16 +49,7 @@ jobs:
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
run: cargo build --locked
|
||||
- name: Fuzz
|
||||
run: |
|
||||
(
|
||||
uvx \
|
||||
--python=3.12 \
|
||||
--from=./python/py-fuzzer \
|
||||
fuzz \
|
||||
--test-executable=target/debug/ruff \
|
||||
--bin=ruff \
|
||||
$(shuf -i 0-9999999999999999999 -n 1000)
|
||||
)
|
||||
run: python scripts/fuzz-parser/fuzz.py $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
|
||||
|
||||
create-issue-on-failure:
|
||||
name: Create an issue if the daily fuzz surfaced any bugs
|
||||
|
||||
4
.github/workflows/pr-comment.yaml
vendored
4
.github/workflows/pr-comment.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
- uses: dawidd6/action-download-artifact@v6
|
||||
name: Download pull request number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
- uses: dawidd6/action-download-artifact@v6
|
||||
name: "Download ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
|
||||
1
.github/workflows/publish-docs.yml
vendored
1
.github/workflows/publish-docs.yml
vendored
@@ -26,7 +26,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
|
||||
4
.github/workflows/publish-playground.yml
vendored
4
.github/workflows/publish-playground.yml
vendored
@@ -25,8 +25,6 @@ jobs:
|
||||
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -49,7 +47,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.13.0
|
||||
uses: cloudflare/wrangler-action@v3.9.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
2
.github/workflows/publish-wasm.yml
vendored
2
.github/workflows/publish-wasm.yml
vendored
@@ -30,8 +30,6 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
|
||||
69
.github/workflows/release.yml
vendored
69
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
@@ -6,7 +6,7 @@
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * builds artifacts with cargo-dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
@@ -24,10 +24,10 @@ permissions:
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# (cargo-dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
@@ -48,7 +48,7 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-20.04"
|
||||
outputs:
|
||||
@@ -62,16 +62,16 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
- name: Install cargo-dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
|
||||
- name: Cache cargo-dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
path: ~/.cargo/bin/cargo-dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
@@ -79,8 +79,8 @@ jobs:
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cargo dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
@@ -124,12 +124,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
- name: Install cached cargo-dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
- run: chmod +x ~/.cargo/bin/cargo-dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -140,8 +140,8 @@ jobs:
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
@@ -174,12 +174,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
- name: Install cached cargo-dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
- run: chmod +x ~/.cargo/bin/cargo-dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
@@ -202,46 +202,15 @@ jobs:
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
|
||||
custom-publish-pypi:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
uses: ./.github/workflows/publish-pypi.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# publish jobs get escalated permissions
|
||||
permissions:
|
||||
"id-token": "write"
|
||||
"packages": "write"
|
||||
|
||||
custom-publish-wasm:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
uses: ./.github/workflows/publish-wasm.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# publish jobs get escalated permissions
|
||||
permissions:
|
||||
"contents": "read"
|
||||
"id-token": "write"
|
||||
"packages": "write"
|
||||
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- custom-publish-pypi
|
||||
- custom-publish-wasm
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-wasm.result == 'skipped' || needs.custom-publish-wasm.result == 'success') }}
|
||||
if: ${{ always() && needs.host.result == 'success' }}
|
||||
runs-on: "ubuntu-20.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -25,13 +25,11 @@ jobs:
|
||||
name: Checkout Ruff
|
||||
with:
|
||||
path: ruff
|
||||
persist-credentials: true
|
||||
- uses: actions/checkout@v4
|
||||
name: Checkout typeshed
|
||||
with:
|
||||
repository: python/typeshed
|
||||
path: typeshed
|
||||
persist-credentials: true
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
@@ -17,12 +17,12 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.23
|
||||
rev: v0.21
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.19
|
||||
rev: 0.7.18
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
@@ -36,7 +36,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.43.0
|
||||
rev: v0.42.0
|
||||
hooks:
|
||||
- id: markdownlint-fix
|
||||
exclude: |
|
||||
@@ -51,15 +51,11 @@ repos:
|
||||
- id: blacken-docs
|
||||
args: ["--pyi", "--line-length", "130"]
|
||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.*?invalid(_.+)*_syntax\.md
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.2
|
||||
rev: v1.26.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -73,7 +69,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.2
|
||||
rev: v0.7.0
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -83,25 +79,10 @@ repos:
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.4.2
|
||||
rev: v3.3.3
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
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
|
||||
# We could consider enabling the low-severity warnings, but they're noisy
|
||||
args: [--min-severity=medium]
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.30.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
ci:
|
||||
skip: [cargo-fmt, dev-generate-all]
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
|
||||
|
||||
- **Changed location of `pydoclint` diagnostics**
|
||||
|
||||
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
|
||||
|
||||
If you've opted into these preview rules but have them suppressed using
|
||||
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
|
||||
some places, this change may mean that you need to move the `noqa` suppression
|
||||
comments. Most users should be unaffected by this change.
|
||||
|
||||
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
|
||||
|
||||
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
|
||||
|
||||
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
|
||||
|
||||
- **Changes to the line width calculation**
|
||||
|
||||
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments
|
||||
@@ -192,7 +167,7 @@ flag or `unsafe-fixes` configuration option can be used to enable unsafe fixes.
|
||||
|
||||
See the [docs](https://docs.astral.sh/ruff/configuration/#fix-safety) for details.
|
||||
|
||||
### Remove formatter-conflicting rules from the default rule set ([#7900](https://github.com/astral-sh/ruff/pull/7900))
|
||||
### Remove formatter-conflicting rules from the default rule set ([#7900](https://github.com/astral-sh/ruff/pull/7900))
|
||||
|
||||
Previously, Ruff enabled all implemented rules in Pycodestyle (`E`) by default. Ruff now only includes the
|
||||
Pycodestyle prefixes `E4`, `E7`, and `E9` to exclude rules that conflict with automatic formatters. Consequently,
|
||||
|
||||
271
CHANGELOG.md
271
CHANGELOG.md
@@ -1,268 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Avoid deprecated values (`AIR302`) ([#14582](https://github.com/astral-sh/ruff/pull/14582))
|
||||
- \[`airflow`\] Extend removed names for `AIR302` ([#14734](https://github.com/astral-sh/ruff/pull/14734))
|
||||
- \[`ruff`\] Extend `unnecessary-regular-expression` to non-literal strings (`RUF055`) ([#14679](https://github.com/astral-sh/ruff/pull/14679))
|
||||
- \[`ruff`\] Implement `used-dummy-variable` (`RUF052`) ([#14611](https://github.com/astral-sh/ruff/pull/14611))
|
||||
- \[`ruff`\] Implement `unnecessary-cast-to-int` (`RUF046`) ([#14697](https://github.com/astral-sh/ruff/pull/14697))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`airflow`\] Check `AIR001` from builtin or providers `operators` module ([#14631](https://github.com/astral-sh/ruff/pull/14631))
|
||||
- \[`flake8-pytest-style`\] Remove `@` in `pytest.mark.parametrize` rule messages ([#14770](https://github.com/astral-sh/ruff/pull/14770))
|
||||
- \[`pandas-vet`\] Skip rules if the `panda` module hasn't been seen ([#14671](https://github.com/astral-sh/ruff/pull/14671))
|
||||
- \[`pylint`\] Fix false negatives for `ascii` and `sorted` in `len-as-condition` (`PLC1802`) ([#14692](https://github.com/astral-sh/ruff/pull/14692))
|
||||
- \[`refurb`\] Guard `hashlib` imports and mark `hashlib-digest-hex` fix as safe (`FURB181`) ([#14694](https://github.com/astral-sh/ruff/pull/14694))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-import-conventions`\] Improve syntax check for aliases supplied in configuration for `unconventional-import-alias` (`ICN001`) ([#14745](https://github.com/astral-sh/ruff/pull/14745))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Revert: [pyflakes] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) (#14615) ([#14726](https://github.com/astral-sh/ruff/pull/14726))
|
||||
- \[`pep8-naming`\] Avoid false positive for `class Bar(type(foo))` (`N804`) ([#14683](https://github.com/astral-sh/ruff/pull/14683))
|
||||
- \[`pycodestyle`\] Handle f-strings properly for `invalid-escape-sequence` (`W605`) ([#14748](https://github.com/astral-sh/ruff/pull/14748))
|
||||
- \[`pylint`\] Ignore `@overload` in `PLR0904` ([#14730](https://github.com/astral-sh/ruff/pull/14730))
|
||||
- \[`refurb`\] Handle non-finite decimals in `verbose-decimal-constructor` (`FURB157`) ([#14596](https://github.com/astral-sh/ruff/pull/14596))
|
||||
- \[`ruff`\] Avoid emitting `assignment-in-assert` when all references to the assigned variable are themselves inside `assert`s (`RUF018`) ([#14661](https://github.com/astral-sh/ruff/pull/14661))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve docs for `flake8-use-pathlib` rules ([#14741](https://github.com/astral-sh/ruff/pull/14741))
|
||||
- Improve error messages and docs for `flake8-comprehensions` rules ([#14729](https://github.com/astral-sh/ruff/pull/14729))
|
||||
- \[`flake8-type-checking`\] Expands `TC006` docs to better explain itself ([#14749](https://github.com/astral-sh/ruff/pull/14749))
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Preview features
|
||||
|
||||
- Formatter: Avoid invalid syntax for format-spec with quotes for all Python versions ([#14625](https://github.com/astral-sh/ruff/pull/14625))
|
||||
- Formatter: Consider quotes inside format-specs when choosing the quotes for an f-string ([#14493](https://github.com/astral-sh/ruff/pull/14493))
|
||||
- Formatter: Do not consider f-strings with escaped newlines as multiline ([#14624](https://github.com/astral-sh/ruff/pull/14624))
|
||||
- Formatter: Fix f-string formatting in assignment statement ([#14454](https://github.com/astral-sh/ruff/pull/14454))
|
||||
- Formatter: Fix unnecessary space around power operator (`**`) in overlong f-string expressions ([#14489](https://github.com/astral-sh/ruff/pull/14489))
|
||||
- \[`airflow`\] Avoid implicit `schedule` argument to `DAG` and `@dag` (`AIR301`) ([#14581](https://github.com/astral-sh/ruff/pull/14581))
|
||||
- \[`flake8-builtins`\] Exempt private built-in modules (`A005`) ([#14505](https://github.com/astral-sh/ruff/pull/14505))
|
||||
- \[`flake8-pytest-style`\] Fix `pytest.mark.parametrize` rules to check calls instead of decorators ([#14515](https://github.com/astral-sh/ruff/pull/14515))
|
||||
- \[`flake8-type-checking`\] Implement `runtime-cast-value` (`TC006`) ([#14511](https://github.com/astral-sh/ruff/pull/14511))
|
||||
- \[`flake8-type-checking`\] Implement `unquoted-type-alias` (`TC007`) and `quoted-type-alias` (`TC008`) ([#12927](https://github.com/astral-sh/ruff/pull/12927))
|
||||
- \[`flake8-use-pathlib`\] Recommend `Path.iterdir()` over `os.listdir()` (`PTH208`) ([#14509](https://github.com/astral-sh/ruff/pull/14509))
|
||||
- \[`pylint`\] Extend `invalid-envvar-default` to detect `os.environ.get` (`PLW1508`) ([#14512](https://github.com/astral-sh/ruff/pull/14512))
|
||||
- \[`pylint`\] Implement `len-test` (`PLC1802`) ([#14309](https://github.com/astral-sh/ruff/pull/14309))
|
||||
- \[`refurb`\] Fix bug where methods defined using lambdas were flagged by `FURB118` ([#14639](https://github.com/astral-sh/ruff/pull/14639))
|
||||
- \[`ruff`\] Auto-add `r` prefix when string has no backslashes for `unraw-re-pattern` (`RUF039`) ([#14536](https://github.com/astral-sh/ruff/pull/14536))
|
||||
- \[`ruff`\] Implement `invalid-assert-message-literal-argument` (`RUF040`) ([#14488](https://github.com/astral-sh/ruff/pull/14488))
|
||||
- \[`ruff`\] Implement `unnecessary-nested-literal` (`RUF041`) ([#14323](https://github.com/astral-sh/ruff/pull/14323))
|
||||
- \[`ruff`\] Implement `unnecessary-regular-expression` (`RUF055`) ([#14659](https://github.com/astral-sh/ruff/pull/14659))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Ignore more rules for stub files ([#14541](https://github.com/astral-sh/ruff/pull/14541))
|
||||
- \[`pep8-naming`\] Eliminate false positives for single-letter names (`N811`, `N814`) ([#14584](https://github.com/astral-sh/ruff/pull/14584))
|
||||
- \[`pyflakes`\] Avoid false positives in `@no_type_check` contexts (`F821`, `F722`) ([#14615](https://github.com/astral-sh/ruff/pull/14615))
|
||||
- \[`ruff`\] Detect redirected-noqa in file-level comments (`RUF101`) ([#14635](https://github.com/astral-sh/ruff/pull/14635))
|
||||
- \[`ruff`\] Mark fixes for `unsorted-dunder-all` and `unsorted-dunder-slots` as unsafe when there are complex comments in the sequence (`RUF022`, `RUF023`) ([#14560](https://github.com/astral-sh/ruff/pull/14560))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid fixing code to `None | None` for `redundant-none-literal` (`PYI061`) and `never-union` (`RUF020`) ([#14583](https://github.com/astral-sh/ruff/pull/14583), [#14589](https://github.com/astral-sh/ruff/pull/14589))
|
||||
- \[`flake8-bugbear`\] Fix `mutable-contextvar-default` to resolve annotated function calls properly (`B039`) ([#14532](https://github.com/astral-sh/ruff/pull/14532))
|
||||
- \[`flake8-pyi`, `ruff`\] Fix traversal of nested literals and unions (`PYI016`, `PYI051`, `PYI055`, `PYI062`, `RUF041`) ([#14641](https://github.com/astral-sh/ruff/pull/14641))
|
||||
- \[`flake8-pyi`\] Avoid rewriting invalid type expressions in `unnecessary-type-union` (`PYI055`) ([#14660](https://github.com/astral-sh/ruff/pull/14660))
|
||||
- \[`flake8-type-checking`\] Avoid syntax errors and type checking problem for quoted annotations autofix (`TC003`, `TC006`) ([#14634](https://github.com/astral-sh/ruff/pull/14634))
|
||||
- \[`pylint`\] Do not wrap function calls in parentheses in the fix for unnecessary-dunder-call (`PLC2801`) ([#14601](https://github.com/astral-sh/ruff/pull/14601))
|
||||
- \[`ruff`\] Handle `attrs`'s `auto_attribs` correctly (`RUF009`) ([#14520](https://github.com/astral-sh/ruff/pull/14520))
|
||||
|
||||
## 0.8.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
|
||||
|
||||
- **Changed location of `pydoclint` diagnostics**
|
||||
|
||||
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
|
||||
|
||||
If you've opted into these preview rules but have them suppressed using
|
||||
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
|
||||
some places, this change may mean that you need to move the `noqa` suppression
|
||||
comments. Most users should be unaffected by this change.
|
||||
|
||||
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
|
||||
|
||||
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
|
||||
|
||||
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
|
||||
|
||||
- **Changes to the line width calculation**
|
||||
|
||||
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
|
||||
|
||||
### Removed Rules
|
||||
|
||||
The following deprecated rules have been removed:
|
||||
|
||||
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
|
||||
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
|
||||
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`)
|
||||
- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`)
|
||||
- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`)
|
||||
- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`)
|
||||
|
||||
### Remapped rules
|
||||
|
||||
The following rules have been remapped to new rule codes:
|
||||
|
||||
- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC`
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`)
|
||||
- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`)
|
||||
- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`)
|
||||
- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`)
|
||||
- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`)
|
||||
- [`pep484-style-positional-only-parameter`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-parameter/) (`PYI063`)
|
||||
- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`)
|
||||
- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`)
|
||||
- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`)
|
||||
- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`)
|
||||
- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`)
|
||||
- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`)
|
||||
- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names.
|
||||
- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available
|
||||
|
||||
The following fixes have been stabilized:
|
||||
|
||||
- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`)
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394))
|
||||
- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393))
|
||||
- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316))
|
||||
- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273))
|
||||
- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373))
|
||||
- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319))
|
||||
- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446))
|
||||
- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474))
|
||||
- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/).
|
||||
- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402))
|
||||
- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371))
|
||||
- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435))
|
||||
- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477))
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-datetimez`\] Detect usages of `datetime.max`/`datetime.min` (`DTZ901`) ([#14288](https://github.com/astral-sh/ruff/pull/14288))
|
||||
- \[`flake8-logging`\] Implement `root-logger-calls` (`LOG015`) ([#14302](https://github.com/astral-sh/ruff/pull/14302))
|
||||
- \[`flake8-no-pep420`\] Detect empty implicit namespace packages (`INP001`) ([#14236](https://github.com/astral-sh/ruff/pull/14236))
|
||||
- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI019`) ([#14238](https://github.com/astral-sh/ruff/pull/14238))
|
||||
- \[`perflint`\] Implement quick-fix for `manual-list-comprehension` (`PERF401`) ([#13919](https://github.com/astral-sh/ruff/pull/13919))
|
||||
- \[`pylint`\] Implement `shallow-copy-environ` (`W1507`) ([#14241](https://github.com/astral-sh/ruff/pull/14241))
|
||||
- \[`ruff`\] Implement `none-not-at-end-of-union` (`RUF036`) ([#14314](https://github.com/astral-sh/ruff/pull/14314))
|
||||
- \[`ruff`\] Implementation `unsafe-markup-call` from `flake8-markupsafe` plugin (`RUF035`) ([#14224](https://github.com/astral-sh/ruff/pull/14224))
|
||||
- \[`ruff`\] Report problems for `attrs` dataclasses (`RUF008`, `RUF009`) ([#14327](https://github.com/astral-sh/ruff/pull/14327))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-boolean-trap`\] Exclude dunder methods that define operators (`FBT001`) ([#14203](https://github.com/astral-sh/ruff/pull/14203))
|
||||
- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI034`) ([#14217](https://github.com/astral-sh/ruff/pull/14217))
|
||||
- \[`flake8-pyi`\] Always autofix `duplicate-union-members` (`PYI016`) ([#14270](https://github.com/astral-sh/ruff/pull/14270))
|
||||
- \[`flake8-pyi`\] Improve autofix for nested and mixed type unions for `unnecessary-type-union` (`PYI055`) ([#14272](https://github.com/astral-sh/ruff/pull/14272))
|
||||
- \[`flake8-pyi`\] Mark fix as unsafe when type annotation contains comments for `duplicate-literal-member` (`PYI062`) ([#14268](https://github.com/astral-sh/ruff/pull/14268))
|
||||
|
||||
### Server
|
||||
|
||||
- Use the current working directory to resolve settings from `ruff.configuration` ([#14352](https://github.com/astral-sh/ruff/pull/14352))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid conflicts between `PLC014` (`useless-import-alias`) and `I002` (`missing-required-import`) by considering `lint.isort.required-imports` for `PLC014` ([#14287](https://github.com/astral-sh/ruff/pull/14287))
|
||||
- \[`flake8-type-checking`\] Skip quoting annotation if it becomes invalid syntax (`TCH001`)
|
||||
- \[`flake8-pyi`\] Avoid using `typing.Self` in stub files pre-Python 3.11 (`PYI034`) ([#14230](https://github.com/astral-sh/ruff/pull/14230))
|
||||
- \[`flake8-pytest-style`\] Flag `pytest.raises` call with keyword argument `expected_exception` (`PT011`) ([#14298](https://github.com/astral-sh/ruff/pull/14298))
|
||||
- \[`flake8-simplify`\] Infer "unknown" truthiness for literal iterables whose items are all unpacks (`SIM222`) ([#14263](https://github.com/astral-sh/ruff/pull/14263))
|
||||
- \[`flake8-type-checking`\] Fix false positives for `typing.Annotated` (`TCH001`) ([#14311](https://github.com/astral-sh/ruff/pull/14311))
|
||||
- \[`pylint`\] Allow `await` at the top-level scope of a notebook (`PLE1142`) ([#14225](https://github.com/astral-sh/ruff/pull/14225))
|
||||
- \[`pylint`\] Fix miscellaneous issues in `await-outside-async` detection (`PLE1142`) ([#14218](https://github.com/astral-sh/ruff/pull/14218))
|
||||
- \[`pyupgrade`\] Avoid applying PEP 646 rewrites in invalid contexts (`UP044`) ([#14234](https://github.com/astral-sh/ruff/pull/14234))
|
||||
- \[`pyupgrade`\] Detect permutations in redundant open modes (`UP015`) ([#14255](https://github.com/astral-sh/ruff/pull/14255))
|
||||
- \[`refurb`\] Avoid triggering `hardcoded-string-charset` for reordered sets (`FURB156`) ([#14233](https://github.com/astral-sh/ruff/pull/14233))
|
||||
- \[`refurb`\] Further special cases added to `verbose-decimal-constructor` (`FURB157`) ([#14216](https://github.com/astral-sh/ruff/pull/14216))
|
||||
- \[`refurb`\] Use `UserString` instead of non-existent `UserStr` (`FURB189`) ([#14209](https://github.com/astral-sh/ruff/pull/14209))
|
||||
- \[`ruff`\] Avoid treating lowercase letters as `# noqa` codes (`RUF100`) ([#14229](https://github.com/astral-sh/ruff/pull/14229))
|
||||
- \[`ruff`\] Do not report when `Optional` has no type arguments (`RUF013`) ([#14181](https://github.com/astral-sh/ruff/pull/14181))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add "Notebook behavior" section for `F704`, `PLE1142` ([#14266](https://github.com/astral-sh/ruff/pull/14266))
|
||||
- Document comment policy around fix safety ([#14300](https://github.com/astral-sh/ruff/pull/14300))
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928))
|
||||
- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059))
|
||||
- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008))
|
||||
- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105))
|
||||
- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064))
|
||||
- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094))
|
||||
- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150))
|
||||
- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188))
|
||||
- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065))
|
||||
- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809))
|
||||
- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069))
|
||||
- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058))
|
||||
- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144))
|
||||
- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097))
|
||||
- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066))
|
||||
- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063))
|
||||
- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146))
|
||||
- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053))
|
||||
- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971))
|
||||
- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040))
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Preview features
|
||||
@@ -294,7 +31,7 @@ The following fixes have been stabilized:
|
||||
### Preview features
|
||||
|
||||
- Fix `E221` and `E222` to flag missing or extra whitespace around `==` operator ([#13890](https://github.com/astral-sh/ruff/pull/13890))
|
||||
- Formatter: Alternate quotes for strings inside f-strings in preview ([#13860](https://github.com/astral-sh/ruff/pull/13860))
|
||||
- Formatter: Alternate quotes for strings inside f-strings in preview ([#13860](https://github.com/astral-sh/ruff/pull/13860))
|
||||
- Formatter: Join implicit concatenated strings when they fit on a line ([#13663](https://github.com/astral-sh/ruff/pull/13663))
|
||||
- \[`pylint`\] Restrict `iteration-over-set` to only work on sets of literals (`PLC0208`) ([#13731](https://github.com/astral-sh/ruff/pull/13731))
|
||||
|
||||
@@ -1155,7 +892,7 @@ The following deprecated CLI commands have been removed:
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bugbear`\] Implement `return-in-generator` (`B901`) ([#11644](https://github.com/astral-sh/ruff/pull/11644))
|
||||
- \[`flake8-pyi`\] Implement `pep484-style-positional-only-parameter` (`PYI063`) ([#11699](https://github.com/astral-sh/ruff/pull/11699))
|
||||
- \[`flake8-pyi`\] Implement `PYI063` ([#11699](https://github.com/astral-sh/ruff/pull/11699))
|
||||
- \[`pygrep_hooks`\] Check blanket ignores via file-level pragmas (`PGH004`) ([#11540](https://github.com/astral-sh/ruff/pull/11540))
|
||||
|
||||
### Rule changes
|
||||
@@ -1309,7 +1046,7 @@ To read more about this exciting milestone, check out our [blog post](https://as
|
||||
### Preview features
|
||||
|
||||
- \[`pycodestyle`\] Ignore end-of-line comments when determining blank line rules ([#11342](https://github.com/astral-sh/ruff/pull/11342))
|
||||
- \[`pylint`\] Detect `pathlib.Path.open` calls in `unspecified-encoding` (`PLW1514`) ([#11288](https://github.com/astral-sh/ruff/pull/11288))
|
||||
- \[`pylint`\] Detect `pathlib.Path.open` calls in `unspecified-encoding` (`PLW1514`) ([#11288](https://github.com/astral-sh/ruff/pull/11288))
|
||||
- \[`flake8-pyi`\] Implement `PYI059` (`generic-not-last-base-class`) ([#11233](https://github.com/astral-sh/ruff/pull/11233))
|
||||
- \[`flake8-pyi`\] Implement `PYI062` (`duplicate-literal-member`) ([#11269](https://github.com/astral-sh/ruff/pull/11269))
|
||||
|
||||
@@ -1684,7 +1421,7 @@ To setup `ruff server` with your editor, refer to the [README.md](https://github
|
||||
- \[`pycodestyle`\] Do not ignore lines before the first logical line in blank lines rules. ([#10382](https://github.com/astral-sh/ruff/pull/10382))
|
||||
- \[`pycodestyle`\] Do not trigger `E225` and `E275` when the next token is a ')' ([#10315](https://github.com/astral-sh/ruff/pull/10315))
|
||||
- \[`pylint`\] Avoid false-positive slot non-assignment for `__dict__` (`PLE0237`) ([#10348](https://github.com/astral-sh/ruff/pull/10348))
|
||||
- Gate f-string struct size test for Rustc < 1.76 ([#10371](https://github.com/astral-sh/ruff/pull/10371))
|
||||
- Gate f-string struct size test for Rustc \< 1.76 ([#10371](https://github.com/astral-sh/ruff/pull/10371))
|
||||
|
||||
### Documentation
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ At a high level, the steps involved in adding a new lint rule are as follows:
|
||||
1. Create a file for your rule (e.g., `crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs`).
|
||||
|
||||
1. In that file, define a violation struct (e.g., `pub struct AssertFalse`). You can grep for
|
||||
`#[derive(ViolationMetadata)]` to see examples.
|
||||
`#[violation]` to see examples.
|
||||
|
||||
1. In that file, define a function that adds the violation to the diagnostic list as appropriate
|
||||
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,
|
||||
@@ -863,7 +863,7 @@ each configuration file.
|
||||
|
||||
The package root is used to determine a file's "module path". Consider, again, `baz.py`. In that
|
||||
case, `./my_project/src/foo` was identified as the package root, so the module path for `baz.py`
|
||||
would resolve to `foo.bar.baz` — as computed by taking the relative path from the package root
|
||||
would resolve to `foo.bar.baz` — as computed by taking the relative path from the package root
|
||||
(inclusive of the root itself). The module path can be thought of as "the path you would use to
|
||||
import the module" (e.g., `import foo.bar.baz`).
|
||||
|
||||
|
||||
806
Cargo.lock
generated
806
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
32
Cargo.toml
32
Cargo.toml
@@ -65,8 +65,7 @@ compact_str = "0.8.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dashmap = { version = "6.0.1" }
|
||||
dir-test = { version = "0.4.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
dir-test = { version = "0.3.0" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
etcetera = { version = "0.8.0" }
|
||||
@@ -82,7 +81,6 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
indexmap = { version = "2.6.0" }
|
||||
indicatif = { version = "0.17.8" }
|
||||
indoc = { version = "2.0.4" }
|
||||
insta = { version = "1.35.1" }
|
||||
@@ -103,7 +101,7 @@ matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "7.0.0" }
|
||||
notify = { version = "6.1.1" }
|
||||
ordermap = { version = "0.5.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
@@ -111,7 +109,7 @@ pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.7.1" }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.79" }
|
||||
pyproject-toml = { version = "0.13.4" }
|
||||
pyproject-toml = { version = "0.9.0" }
|
||||
quick-junit = { version = "0.5.0" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
@@ -137,7 +135,7 @@ strum_macros = { version = "0.26.0" }
|
||||
syn = { version = "2.0.55" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
thiserror = { version = "1.0.58" }
|
||||
tikv-jemallocator = { version = "0.6.0" }
|
||||
toml = { version = "0.8.11" }
|
||||
tracing = { version = "0.1.40" }
|
||||
@@ -151,7 +149,7 @@ tracing-tree = { version = "0.4.0" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
unicode-width = { version = "0.2.0" }
|
||||
unicode-width = { version = "0.1.11" }
|
||||
unicode_names2 = { version = "1.2.2" }
|
||||
unicode-normalization = { version = "0.1.23" }
|
||||
ureq = { version = "2.9.6" }
|
||||
@@ -248,10 +246,10 @@ debug = 1
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
# Config for 'dist'
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.25.2-prerelease.3"
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.22.1"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
@@ -282,25 +280,29 @@ targets = [
|
||||
]
|
||||
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
|
||||
auto-includes = false
|
||||
# Whether dist should create a Github Release or use an existing draft
|
||||
# Whether cargo-dist should create a GitHub Release or use an existing draft
|
||||
create-release = true
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "skip"
|
||||
# Whether CI should trigger releases with dispatches instead of tag pushes
|
||||
dispatch-releases = true
|
||||
# Which phase dist should use to create the GitHub release
|
||||
# Which phase cargo-dist should use to create the GitHub release
|
||||
github-release = "announce"
|
||||
# Whether CI should include auto-generated code to build local artifacts
|
||||
build-local-artifacts = false
|
||||
# Local artifacts jobs to run in CI
|
||||
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
post-announce-jobs = [
|
||||
"./notify-dependents",
|
||||
"./publish-docs",
|
||||
"./publish-playground",
|
||||
]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
# Path that installers should place binaries in
|
||||
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
|
||||
install-path = "CARGO_HOME"
|
||||
|
||||
14
README.md
14
README.md
@@ -119,10 +119,6 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
|
||||
|
||||
```shell
|
||||
# With uv.
|
||||
uv add --dev ruff # to add ruff to your project
|
||||
uv tool install ruff # to install ruff globally
|
||||
|
||||
# With pip.
|
||||
pip install ruff
|
||||
|
||||
@@ -140,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.8.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.7.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.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 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.8.2
|
||||
rev: v0.7.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -242,8 +238,8 @@ exclude = [
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.9
|
||||
target-version = "py39"
|
||||
# Assume Python 3.8
|
||||
target-version = "py38"
|
||||
|
||||
[lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
[files]
|
||||
# https://github.com/crate-ci/typos/issues/868
|
||||
extend-exclude = [
|
||||
"crates/red_knot_vendored/vendor/**/*",
|
||||
"**/resources/**/*",
|
||||
"**/snapshots/**/*",
|
||||
"crates/red_knot_workspace/src/workspace/pyproject/package_name.rs"
|
||||
]
|
||||
extend-exclude = ["crates/red_knot_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
|
||||
|
||||
[default.extend-words]
|
||||
"arange" = "arange" # e.g. `numpy.arange`
|
||||
|
||||
36
clippy.toml
36
clippy.toml
@@ -1,25 +1,21 @@
|
||||
doc-valid-idents = [
|
||||
"..",
|
||||
"CodeQL",
|
||||
"FastAPI",
|
||||
"IPython",
|
||||
"LangChain",
|
||||
"LibCST",
|
||||
"McCabe",
|
||||
"NumPy",
|
||||
"SCREAMING_SNAKE_CASE",
|
||||
"SQLAlchemy",
|
||||
"StackOverflow",
|
||||
"PyCharm",
|
||||
"SNMPv1",
|
||||
"SNMPv2",
|
||||
"SNMPv3",
|
||||
"PyFlakes"
|
||||
"..",
|
||||
"CodeQL",
|
||||
"FastAPI",
|
||||
"IPython",
|
||||
"LangChain",
|
||||
"LibCST",
|
||||
"McCabe",
|
||||
"NumPy",
|
||||
"SCREAMING_SNAKE_CASE",
|
||||
"SQLAlchemy",
|
||||
"StackOverflow",
|
||||
"PyCharm",
|
||||
]
|
||||
|
||||
ignore-interior-mutability = [
|
||||
# Interned is read-only. The wrapped `Rc` never gets updated.
|
||||
"ruff_formatter::format_element::Interned",
|
||||
# The expression is read-only.
|
||||
"ruff_python_ast::hashable::HashableExpr",
|
||||
# Interned is read-only. The wrapped `Rc` never gets updated.
|
||||
"ruff_formatter::format_element::Interned",
|
||||
# The expression is read-only.
|
||||
"ruff_python_ast::hashable::HashableExpr",
|
||||
]
|
||||
|
||||
@@ -34,7 +34,6 @@ tracing-tree = { workspace = true }
|
||||
[dev-dependencies]
|
||||
filetime = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -103,7 +103,7 @@ called **once**.
|
||||
|
||||
## Profiling
|
||||
|
||||
Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`.
|
||||
Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`.
|
||||
|
||||
```bash
|
||||
RED_KNOT_LOG_PROFILE=1 red_knot -- --current-directory=../test -vvv
|
||||
|
||||
@@ -5,6 +5,8 @@ use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
@@ -12,9 +14,7 @@ use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
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};
|
||||
@@ -183,10 +183,10 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
|
||||
let system = OsSystem::new(cwd.clone());
|
||||
let cli_configuration = args.to_configuration(&cwd);
|
||||
let workspace_metadata = WorkspaceMetadata::discover(
|
||||
let workspace_metadata = WorkspaceMetadata::from_path(
|
||||
system.current_directory(),
|
||||
&system,
|
||||
Some(&cli_configuration),
|
||||
Some(cli_configuration.clone()),
|
||||
)?;
|
||||
|
||||
// TODO: Use the `program_settings` to compute the key for the database's persistent
|
||||
@@ -318,9 +318,8 @@ impl MainLoop {
|
||||
} => {
|
||||
let has_diagnostics = !result.is_empty();
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
for diagnostic in result {
|
||||
println!("{}", diagnostic.display(db));
|
||||
tracing::error!("{}", diagnostic);
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -379,10 +378,7 @@ impl MainLoopCancellationToken {
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
result: Vec<Box<dyn Diagnostic>>,
|
||||
revision: u64,
|
||||
},
|
||||
CheckCompleted { result: Vec<String>, revision: u64 },
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
Py38,
|
||||
#[default]
|
||||
Py38,
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
@@ -46,17 +46,3 @@ impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
|
||||
use red_knot_workspace::db::{Db, RootDatabase};
|
||||
use red_knot_workspace::watch::{directory_watcher, ChangeEvent, WorkspaceWatcher};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
@@ -17,7 +19,7 @@ use ruff_db::Upcast;
|
||||
struct TestCase {
|
||||
db: RootDatabase,
|
||||
watcher: Option<WorkspaceWatcher>,
|
||||
changes_receiver: crossbeam::channel::Receiver<Vec<ChangeEvent>>,
|
||||
changes_receiver: crossbeam::channel::Receiver<Vec<watch::ChangeEvent>>,
|
||||
/// The temporary directory that contains the test files.
|
||||
/// We need to hold on to it in the test case or the temp files get deleted.
|
||||
_temp_dir: tempfile::TempDir,
|
||||
@@ -38,87 +40,45 @@ impl TestCase {
|
||||
&self.db
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn stop_watch<M>(&mut self, matcher: M) -> Vec<ChangeEvent>
|
||||
where
|
||||
M: MatchEvent,
|
||||
{
|
||||
// track_caller is unstable for lambdas -> That's why this is a fn
|
||||
#[track_caller]
|
||||
fn panic_with_formatted_events(events: Vec<ChangeEvent>) -> Vec<ChangeEvent> {
|
||||
panic!(
|
||||
"Didn't observe expected change:\n{}",
|
||||
events
|
||||
.into_iter()
|
||||
.map(|event| format!(" - {event:?}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
self.try_stop_watch(matcher, Duration::from_secs(10))
|
||||
.unwrap_or_else(panic_with_formatted_events)
|
||||
fn stop_watch(&mut self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_stop_watch(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none")
|
||||
}
|
||||
|
||||
fn try_stop_watch<M>(
|
||||
&mut self,
|
||||
mut matcher: M,
|
||||
timeout: Duration,
|
||||
) -> Result<Vec<ChangeEvent>, Vec<ChangeEvent>>
|
||||
where
|
||||
M: MatchEvent,
|
||||
{
|
||||
tracing::debug!("Try stopping watch with timeout {:?}", timeout);
|
||||
|
||||
fn try_stop_watch(&mut self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
let watcher = self
|
||||
.watcher
|
||||
.take()
|
||||
.expect("Cannot call `stop_watch` more than once");
|
||||
|
||||
let start = Instant::now();
|
||||
let mut all_events = Vec::new();
|
||||
|
||||
loop {
|
||||
let events = self
|
||||
.changes_receiver
|
||||
.recv_timeout(Duration::from_millis(100))
|
||||
.unwrap_or_default();
|
||||
|
||||
if events
|
||||
.iter()
|
||||
.any(|event| matcher.match_event(event) || event.is_rescan())
|
||||
{
|
||||
all_events.extend(events);
|
||||
break;
|
||||
}
|
||||
|
||||
all_events.extend(events);
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(all_events);
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
watcher.flush();
|
||||
tracing::debug!("Flushed file watcher");
|
||||
watcher.stop();
|
||||
tracing::debug!("Stopping file watcher");
|
||||
|
||||
// Consume remaining events
|
||||
for event in &self.changes_receiver {
|
||||
all_events.extend(event);
|
||||
}
|
||||
|
||||
Ok(all_events)
|
||||
if all_events.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
fn take_watch_changes(&self) -> Vec<ChangeEvent> {
|
||||
#[cfg(unix)]
|
||||
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_take_watch_changes(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none")
|
||||
}
|
||||
|
||||
fn try_take_watch_changes(&self, timeout: Duration) -> Option<Vec<ChangeEvent>> {
|
||||
let watcher = self.watcher.as_ref()?;
|
||||
fn try_take_watch_changes(&self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
let Some(watcher) = &self.watcher else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
@@ -140,7 +100,7 @@ impl TestCase {
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
fn apply_changes(&mut self, changes: Vec<ChangeEvent>) {
|
||||
fn apply_changes(&mut self, changes: Vec<watch::ChangeEvent>) {
|
||||
self.db.apply_changes(changes, Some(&self.configuration));
|
||||
}
|
||||
|
||||
@@ -150,8 +110,8 @@ impl TestCase {
|
||||
) -> anyhow::Result<()> {
|
||||
let program = Program::get(self.db());
|
||||
|
||||
let new_settings = configuration.to_settings(self.db.workspace().root(&self.db));
|
||||
self.configuration.search_paths = configuration;
|
||||
self.configuration.search_paths = configuration.clone();
|
||||
let new_settings = configuration.into_settings(self.db.workspace().root(&self.db));
|
||||
|
||||
program.update_search_paths(&mut self.db, &new_settings)?;
|
||||
|
||||
@@ -176,23 +136,6 @@ impl TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
trait MatchEvent {
|
||||
fn match_event(&mut self, event: &ChangeEvent) -> bool;
|
||||
}
|
||||
|
||||
fn event_for_file(name: &str) -> impl MatchEvent + '_ {
|
||||
|event: &ChangeEvent| event.file_name() == Some(name)
|
||||
}
|
||||
|
||||
impl<F> MatchEvent for F
|
||||
where
|
||||
F: FnMut(&ChangeEvent) -> bool,
|
||||
{
|
||||
fn match_event(&mut self, event: &ChangeEvent) -> bool {
|
||||
(*self)(event)
|
||||
}
|
||||
}
|
||||
|
||||
trait SetupFiles {
|
||||
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()>;
|
||||
}
|
||||
@@ -261,9 +204,7 @@ where
|
||||
.as_utf8_path()
|
||||
.canonicalize_utf8()
|
||||
.with_context(|| "Failed to canonicalize root path.")?,
|
||||
)
|
||||
.simplified()
|
||||
.to_path_buf();
|
||||
);
|
||||
|
||||
let workspace_path = root_path.join("workspace");
|
||||
|
||||
@@ -300,7 +241,8 @@ where
|
||||
search_paths,
|
||||
};
|
||||
|
||||
let workspace = WorkspaceMetadata::discover(&workspace_path, &system, Some(&configuration))?;
|
||||
let workspace =
|
||||
WorkspaceMetadata::from_path(&workspace_path, &system, Some(configuration.clone()))?;
|
||||
|
||||
let db = RootDatabase::new(workspace, system)?;
|
||||
|
||||
@@ -368,7 +310,7 @@ fn new_file() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -391,7 +333,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -413,7 +355,7 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
|
||||
update_file(&foo_path, "print('Version 2')")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
assert!(!changes.is_empty());
|
||||
|
||||
@@ -438,7 +380,7 @@ fn deleted_file() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::remove_file(foo_path.as_std_path())?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -470,7 +412,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
||||
trash_path.join("foo.py").as_std_path(),
|
||||
)?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -502,7 +444,7 @@ fn move_file_to_workspace() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::rename(foo_path.as_std_path(), foo_in_workspace_path.as_std_path())?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -530,7 +472,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::rename(foo_path.as_std_path(), bar_path.as_std_path())?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("bar.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -574,7 +516,7 @@ fn directory_moved_to_workspace() -> anyhow::Result<()> {
|
||||
std::fs::rename(sub_original_path.as_std_path(), sub_new_path.as_std_path())
|
||||
.with_context(|| "Failed to move sub directory")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -633,7 +575,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
std::fs::rename(sub_path.as_std_path(), trashed_sub.as_std_path())
|
||||
.with_context(|| "Failed to move the sub directory to the trash")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -695,8 +637,7 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
std::fs::rename(sub_path.as_std_path(), foo_baz.as_std_path())
|
||||
.with_context(|| "Failed to move the sub directory")?;
|
||||
|
||||
// Linux and windows only emit an event for the newly created root directory, but not for every new component.
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -769,7 +710,7 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
std::fs::remove_dir_all(sub_path.as_std_path())
|
||||
.with_context(|| "Failed to remove the sub directory")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -806,7 +747,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("a.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -837,7 +778,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("a.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -866,9 +807,9 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
let changes = case.try_stop_watch(|_: &ChangeEvent| true, Duration::from_millis(100));
|
||||
let changes = case.try_stop_watch(Duration::from_millis(100));
|
||||
|
||||
assert_eq!(changes, Err(vec![]));
|
||||
assert_eq!(changes, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -906,7 +847,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
"os: 3.0-",
|
||||
)?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("VERSIONS"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -959,7 +900,7 @@ fn hard_links_in_workspace() -> anyhow::Result<()> {
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -1030,7 +971,7 @@ fn hard_links_to_target_outside_workspace() -> anyhow::Result<()> {
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
|
||||
let changes = case.stop_watch(ChangeEvent::is_changed);
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -1069,7 +1010,7 @@ mod unix {
|
||||
)
|
||||
.with_context(|| "Failed to set file permissions.")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -1167,7 +1108,7 @@ mod unix {
|
||||
update_file(baz_workspace, "def baz(): print('Version 3')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -1238,7 +1179,7 @@ mod unix {
|
||||
update_file(&patched_bar_baz, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -1346,7 +1287,7 @@ mod unix {
|
||||
update_file(&baz_original, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
@@ -1370,137 +1311,3 @@ mod unix {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_packages_delete_root() -> anyhow::Result<()> {
|
||||
let mut case = setup(|root: &SystemPath, workspace_root: &SystemPath| {
|
||||
std::fs::write(
|
||||
workspace_root.join("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
root.join("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "outer"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
assert_eq!(
|
||||
case.db().workspace().root(case.db()),
|
||||
&*case.workspace_path("")
|
||||
);
|
||||
|
||||
std::fs::remove_file(case.workspace_path("pyproject.toml").as_std_path())?;
|
||||
|
||||
let changes = case.stop_watch(ChangeEvent::is_deleted);
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
// It should now pick up the outer workspace.
|
||||
assert_eq!(case.db().workspace().root(case.db()), case.root_path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_package() -> anyhow::Result<()> {
|
||||
let mut case = setup([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
|
||||
[tool.knot.workspace]
|
||||
members = ["packages/*"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"packages/a/pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "a"
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
|
||||
|
||||
std::fs::create_dir(case.workspace_path("packages/b").as_std_path())
|
||||
.context("failed to create folder for package 'b'")?;
|
||||
|
||||
// It seems that the file watcher won't pick up on file changes shortly after the folder
|
||||
// was created... I suspect this is because most file watchers don't support recursive
|
||||
// file watching. Instead, file-watching libraries manually implement recursive file watching
|
||||
// by setting a watcher for each directory. But doing this obviously "lags" behind.
|
||||
case.take_watch_changes();
|
||||
|
||||
std::fs::write(
|
||||
case.workspace_path("packages/b/pyproject.toml")
|
||||
.as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "b"
|
||||
"#,
|
||||
)
|
||||
.context("failed to write pyproject.toml for package b")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("pyproject.toml"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_package() -> anyhow::Result<()> {
|
||||
let mut case = setup([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
|
||||
[tool.knot.workspace]
|
||||
members = ["packages/*"]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"packages/a/pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "a"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"packages/b/pyproject.toml",
|
||||
r#"
|
||||
[project]
|
||||
name = "b"
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
|
||||
|
||||
std::fs::remove_dir_all(case.workspace_path("packages/b").as_std_path())
|
||||
.context("failed to remove package 'b'")?;
|
||||
|
||||
let changes = case.stop_watch(ChangeEvent::is_deleted);
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
@@ -26,15 +24,13 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
itertools = { workspace = true}
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
@@ -47,11 +43,10 @@ red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
dir-test = { workspace = true }
|
||||
dir-test = {workspace = true}
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
quickcheck = { version = "1.0.3", default-features = false }
|
||||
quickcheck_macros = { version = "1.0.0" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# Any
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Any` is a way to name the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
x: Any = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Aliased to a different name
|
||||
|
||||
If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any as RenamedAny
|
||||
|
||||
x: RenamedAny = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Shadowed class
|
||||
|
||||
If you define your own class named `Any`, using that in a type expression refers to your class, and
|
||||
isn't a spelling of the Any type.
|
||||
|
||||
```py
|
||||
class Any:
|
||||
pass
|
||||
|
||||
x: Any
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
|
||||
# This verifies that we're not accidentally seeing typing.Any, since str is assignable
|
||||
# to that but not to our locally defined class.
|
||||
y: Any = "not an Any" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
The spec allows you to define subclasses of `Any`.
|
||||
|
||||
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
|
||||
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
|
||||
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
|
||||
assignable to `int`.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Subclass(Any):
|
||||
pass
|
||||
|
||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||
|
||||
x: Subclass = 1 # error: [invalid-assignment]
|
||||
# TODO: no diagnostic
|
||||
y: int = Subclass() # error: [invalid-assignment]
|
||||
|
||||
def f() -> Subclass:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Subclass
|
||||
```
|
||||
@@ -1,152 +0,0 @@
|
||||
# `LiteralString`
|
||||
|
||||
`LiteralString` represents a string that is either defined directly within the source code or is
|
||||
made up of such components.
|
||||
|
||||
Parts of the testcases defined here were adapted from [the specification's examples][1].
|
||||
|
||||
## Usages
|
||||
|
||||
### Valid places
|
||||
|
||||
It can be used anywhere a type is accepted:
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
x: LiteralString
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Within `Literal`
|
||||
|
||||
`LiteralString` cannot be used within `Literal`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
|
||||
```
|
||||
|
||||
### Parametrized
|
||||
|
||||
`LiteralString` cannot be parametrized.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
a: LiteralString[str] # error: [invalid-type-parameter]
|
||||
b: LiteralString["foo"] # error: [invalid-type-parameter]
|
||||
```
|
||||
|
||||
### As a base class
|
||||
|
||||
Subclassing `LiteralString` leads to a runtime error.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
class C(LiteralString): ... # error: [invalid-base]
|
||||
```
|
||||
|
||||
## Inference
|
||||
|
||||
### Common operations
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
foo: LiteralString = "foo"
|
||||
reveal_type(foo) # revealed: Literal["foo"]
|
||||
|
||||
bar: LiteralString = "bar"
|
||||
reveal_type(foo + bar) # revealed: Literal["foobar"]
|
||||
|
||||
baz: LiteralString = "baz"
|
||||
baz += foo
|
||||
reveal_type(baz) # revealed: Literal["bazfoo"]
|
||||
|
||||
qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
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(call todo)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
`Literal[""]` is assignable to `LiteralString`, and `LiteralString` is assignable to `str`, but not
|
||||
vice versa.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
foo_1: Literal["foo"] = "foo"
|
||||
bar_1: LiteralString = foo_1 # fine
|
||||
|
||||
foo_2 = "foo" if coinflip() else "bar"
|
||||
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
|
||||
bar_2: LiteralString = foo_2 # fine
|
||||
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
|
||||
baz_1: str = str()
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if coinflip() else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Narrowing
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
lorem: LiteralString = "lorem" * 1_000_000_000
|
||||
|
||||
reveal_type(lorem) # revealed: LiteralString
|
||||
|
||||
if lorem == "ipsum":
|
||||
reveal_type(lorem) # revealed: Literal["ipsum"]
|
||||
|
||||
reveal_type(lorem) # revealed: LiteralString
|
||||
|
||||
if "" < lorem == "ipsum":
|
||||
reveal_type(lorem) # revealed: Literal["ipsum"]
|
||||
```
|
||||
|
||||
## `typing.LiteralString`
|
||||
|
||||
`typing.LiteralString` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
|
||||
x: LiteralString = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
```
|
||||
|
||||
[1]: https://typing.readthedocs.io/en/latest/spec/literal.html#literalstring
|
||||
@@ -1,64 +0,0 @@
|
||||
# NoReturn & Never
|
||||
|
||||
`NoReturn` is used to annotate the return type for functions that never return. `Never` is the
|
||||
bottom type, representing the empty set of Python objects. These two annotations can be used
|
||||
interchangeably.
|
||||
|
||||
## Function Return Type Annotation
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
def stop() -> NoReturn:
|
||||
raise RuntimeError("no way")
|
||||
|
||||
# revealed: Never
|
||||
reveal_type(stop())
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
a2: Never
|
||||
b1: Any
|
||||
b2: int
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(a1)
|
||||
# revealed: Never
|
||||
reveal_type(a2)
|
||||
|
||||
# Never is assignable to all types.
|
||||
v1: int = a1
|
||||
v2: str = a1
|
||||
# Other types are not assignable to Never except for Never (and Any).
|
||||
v3: Never = b1
|
||||
v4: Never = a2
|
||||
v5: Any = b2
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`"
|
||||
v6: Never = 1
|
||||
```
|
||||
|
||||
## `typing.Never`
|
||||
|
||||
`typing.Never` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
|
||||
x: Never
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
# Optional
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Optional` is equivalent to using the type with a None in a Union.
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
a: Optional[int]
|
||||
a1: Optional[bool]
|
||||
a2: Optional[Optional[bool]]
|
||||
a3: Optional[None]
|
||||
|
||||
def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
# revealed: bool | None
|
||||
reveal_type(a1)
|
||||
# revealed: bool | None
|
||||
reveal_type(a2)
|
||||
# revealed: None
|
||||
reveal_type(a3)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
a: Optional[int] = 1
|
||||
a = None
|
||||
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int | None`"
|
||||
a = ""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Optional
|
||||
|
||||
a: Optional[int]
|
||||
|
||||
def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
# Starred expression annotations
|
||||
|
||||
Type annotations for `*args` can be starred expressions themselves:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVarTuple
|
||||
|
||||
Ts = TypeVarTuple("Ts")
|
||||
|
||||
def append_int(*args: *Ts) -> tuple[*Ts, int]:
|
||||
# TODO: tuple[*Ts]
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
return (*args, 1)
|
||||
|
||||
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
@@ -1,219 +0,0 @@
|
||||
# String annotations
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f() -> "int":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
def f() -> "'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Type expression
|
||||
|
||||
```py
|
||||
def f1() -> "int | str":
|
||||
return 1
|
||||
|
||||
def f2() -> "tuple[int, str]":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: int | str
|
||||
reveal_type(f2()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Partial
|
||||
|
||||
```py
|
||||
def f() -> tuple[int, "str"]:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Deferred
|
||||
|
||||
```py
|
||||
def f() -> "Foo":
|
||||
return Foo()
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred (undefined)
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
def f() -> "Foo":
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Partial deferred
|
||||
|
||||
```py
|
||||
def f() -> int | "Foo":
|
||||
return 1
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: int | Foo
|
||||
```
|
||||
|
||||
## `typing.Literal`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f1() -> Literal["Foo", "Bar"]:
|
||||
return "Foo"
|
||||
|
||||
def f2() -> 'Literal["Foo", "Bar"]':
|
||||
return "Foo"
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
|
||||
```
|
||||
|
||||
## Various string kinds
|
||||
|
||||
```py
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
def f1() -> r"int":
|
||||
return 1
|
||||
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
def f2() -> f"int":
|
||||
return 1
|
||||
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
def f3() -> b"int":
|
||||
return 1
|
||||
|
||||
def f4() -> "int":
|
||||
return 1
|
||||
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
def f5() -> "in" "t":
|
||||
return 1
|
||||
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
def f6() -> "\N{LATIN SMALL LETTER I}nt":
|
||||
return 1
|
||||
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
def f7() -> "\x69nt":
|
||||
return 1
|
||||
|
||||
def f8() -> """int""":
|
||||
return 1
|
||||
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
def f9() -> "b'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: Unknown
|
||||
reveal_type(f2()) # revealed: Unknown
|
||||
reveal_type(f3()) # revealed: Unknown
|
||||
reveal_type(f4()) # revealed: int
|
||||
reveal_type(f5()) # revealed: Unknown
|
||||
reveal_type(f6()) # revealed: Unknown
|
||||
reveal_type(f7()) # revealed: Unknown
|
||||
reveal_type(f8()) # revealed: int
|
||||
reveal_type(f9()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Various string kinds in `typing.Literal`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
|
||||
return "normal"
|
||||
|
||||
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
|
||||
```py
|
||||
MyType = int
|
||||
|
||||
class Aliases:
|
||||
MyType = str
|
||||
|
||||
forward: "MyType"
|
||||
not_forward: MyType
|
||||
|
||||
reveal_type(Aliases.forward) # revealed: str
|
||||
reveal_type(Aliases.not_forward) # revealed: str
|
||||
```
|
||||
|
||||
## Annotated assignment
|
||||
|
||||
```py
|
||||
a: "int" = 1
|
||||
b: "'int'" = 1
|
||||
c: "Foo"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
|
||||
d: "Foo" = 1
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
c = Foo()
|
||||
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[1]
|
||||
reveal_type(c) # revealed: Foo
|
||||
reveal_type(d) # revealed: Foo
|
||||
```
|
||||
|
||||
## Parameter
|
||||
|
||||
TODO: Add tests once parameter inference is supported
|
||||
|
||||
## Invalid expressions
|
||||
|
||||
The expressions in these string annotations aren't valid expressions in this context but we
|
||||
shouldn't panic.
|
||||
|
||||
```py
|
||||
a: "1 or 2"
|
||||
b: "(x := 1)"
|
||||
c: "1 + 2"
|
||||
d: "lambda x: x"
|
||||
e: "x if True else y"
|
||||
f: "{'a': 1, 'b': 2}"
|
||||
g: "{1, 2}"
|
||||
h: "[i for i in range(5)]"
|
||||
i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
m: "yield 1"
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
r: "[1, 2]"
|
||||
s: "(1, 2)"
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
# Union
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Union` can be used to construct union types same as `|` operator.
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a1: Union[int, bool]
|
||||
a2: Union[int, Union[float, str]]
|
||||
a3: Union[int, None]
|
||||
a4: Union[Union[float, str]]
|
||||
a5: Union[int]
|
||||
a6: Union[()]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
|
||||
# revealed: int
|
||||
reveal_type(a1)
|
||||
# revealed: int | float | str
|
||||
reveal_type(a2)
|
||||
# revealed: int | None
|
||||
reveal_type(a3)
|
||||
# revealed: float | str
|
||||
reveal_type(a4)
|
||||
# revealed: int
|
||||
reveal_type(a5)
|
||||
# revealed: Never
|
||||
reveal_type(a6)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a = 1
|
||||
a = ""
|
||||
a1: Union[int, bool]
|
||||
a1 = 1
|
||||
a1 = True
|
||||
# error: [invalid-assignment] "Object of type `Literal[b""]` is not assignable to `int | str`"
|
||||
a = b""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Union
|
||||
|
||||
a: Union[int, str]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
```
|
||||
@@ -50,13 +50,13 @@ reveal_type(b) # revealed: tuple[int]
|
||||
reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogeneous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
# TODO: homogenous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(g) # revealed: @Todo
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(h) # revealed: @Todo
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
@@ -110,29 +110,3 @@ c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = ((
|
||||
# error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `tuple[tuple[int, int], int]`"
|
||||
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = "foo"
|
||||
```
|
||||
|
||||
## Future annotations are deferred
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
x: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
## Annotations in stub files are deferred
|
||||
|
||||
```pyi path=main.pyi
|
||||
x: Foo
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
@@ -85,7 +85,7 @@ f = Foo()
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
## Partially bound with `__add__`
|
||||
@@ -104,7 +104,8 @@ class Foo:
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | str
|
||||
# TODO(charlie): This should be `int | str`, since `__iadd__` may be unbound.
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
## Partially bound target union
|
||||
@@ -126,7 +127,8 @@ else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
# TODO(charlie): This should be `str | int | float`
|
||||
reveal_type(f) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Target union
|
||||
@@ -147,36 +149,6 @@ else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | float
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
# TODO(charlie): This should be `str | float`.
|
||||
reveal_type(f) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -18,3 +18,42 @@ Note: in this particular example, one could argue that the most likely error wou
|
||||
of the `x`/`foo` definitions, and so it could be desirable to infer `Literal[1]` for the type of
|
||||
`x`. On the other hand, there might be a variable `fob` a little higher up in this file, and the
|
||||
actual error might have been just a typo. Inferring `Unknown` thus seems like the safest option.
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
y = x
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
```
|
||||
|
||||
@@ -9,128 +9,12 @@ def bool_instance() -> bool:
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
class C1:
|
||||
class C:
|
||||
x = 1
|
||||
|
||||
else:
|
||||
class C1:
|
||||
class C:
|
||||
x = 2
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
x = 3
|
||||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited attributes
|
||||
|
||||
```py
|
||||
class A:
|
||||
X = "foo"
|
||||
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(C.X) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
## Inherited attributes (multiple inheritance)
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
|
||||
class F(O):
|
||||
X = 56
|
||||
|
||||
class E(O):
|
||||
X = 42
|
||||
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
reveal_type(A.X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Unions with possibly unbound paths
|
||||
|
||||
### Definite boundness within a class
|
||||
|
||||
In this example, the `x` attribute is not defined in the `C2` element of the union:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C2: ...
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
|
||||
We raise the same diagnostic if the attribute is possibly-unbound in at least one element of the
|
||||
union:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C2:
|
||||
if bool_instance():
|
||||
x = 2
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## Unions with all paths unbound
|
||||
|
||||
If the symbol is unbound in all elements of the union, we detect that:
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C1: ...
|
||||
class C2: ...
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
C = C1 if flag else C2
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
reveal_type(C.x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
@@ -202,7 +202,11 @@ reveal_type(A() + B()) # revealed: MyString
|
||||
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(A() + C()) # revealed: MyString
|
||||
# TODO: we currently only understand direct subclasses as subtypes of the superclass.
|
||||
# We need to iterate through the full MRO rather than just the class's bases;
|
||||
# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being
|
||||
# `MyString` rather than `str`
|
||||
reveal_type(A() + C()) # revealed: str
|
||||
```
|
||||
|
||||
## Reflected precedence 2
|
||||
@@ -317,7 +321,7 @@ reveal_type(1 + A()) # revealed: int
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type)
|
||||
reveal_type("foo" + A()) # revealed: @Todo
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -325,7 +329,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo(return type)
|
||||
reveal_type(() + A()) # revealed: @Todo
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
@@ -334,7 +338,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
@@ -18,58 +18,3 @@ class Unit: ...
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class PossiblyNotCallable:
|
||||
if flag():
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
|
||||
```py
|
||||
class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `NonCallable` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class NonCallable:
|
||||
if flag():
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
@@ -16,16 +16,7 @@ async def get_int_async() -> int:
|
||||
return 42
|
||||
|
||||
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
|
||||
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
||||
```
|
||||
|
||||
## Generic
|
||||
|
||||
```py
|
||||
def get_int[T]() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(get_int()) # revealed: int
|
||||
reveal_type(get_int_async()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Decorated
|
||||
@@ -44,7 +35,7 @@ def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo(return type)
|
||||
reveal_type(bar()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
@@ -53,16 +44,3 @@ reveal_type(bar()) # revealed: @Todo(return type)
|
||||
nonsense = 123
|
||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
```
|
||||
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Identity tests
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_a() -> A: ...
|
||||
def get_object() -> object: ...
|
||||
|
||||
a1 = get_a()
|
||||
a2 = get_a()
|
||||
|
||||
n1 = None
|
||||
n2 = None
|
||||
|
||||
o = get_object()
|
||||
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
```
|
||||
@@ -93,11 +93,13 @@ class AlwaysFalse:
|
||||
def __contains__(self, item: int) -> Literal[""]:
|
||||
return ""
|
||||
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: Literal[True]
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: Literal[False]
|
||||
# TODO: it should be Literal[True] and Literal[False]
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: @Todo
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: @Todo
|
||||
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: Literal[False]
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: Literal[True]
|
||||
# TODO: it should be Literal[False] and Literal[True]
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: @Todo
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: @Todo
|
||||
```
|
||||
|
||||
## No Fallback for `__contains__`
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# Comparison: Intersections
|
||||
|
||||
## Positive contributions
|
||||
|
||||
If we have an intersection type `A & B` and we get a definitive true/false answer for one of the
|
||||
types, we can infer that the result for the intersection type is also true/false:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def get_base() -> Base: ...
|
||||
|
||||
x = get_base()
|
||||
c1 = Child1()
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
|
||||
Negative contributions to the intersection type only allow simplifications in a few special cases
|
||||
(equality and identity comparisons).
|
||||
|
||||
### Equality comparisons
|
||||
|
||||
#### Literal strings
|
||||
|
||||
```py
|
||||
x = "x" * 1_000_000_000
|
||||
y = "y" * 1_000_000_000
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
|
||||
if x != "abc":
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||
|
||||
reveal_type(x == "abc") # revealed: Literal[False]
|
||||
reveal_type("abc" == x) # revealed: Literal[False]
|
||||
reveal_type(x == "something else") # revealed: bool
|
||||
reveal_type("something else" == x) # revealed: bool
|
||||
|
||||
reveal_type(x != "abc") # revealed: Literal[True]
|
||||
reveal_type("abc" != x) # revealed: Literal[True]
|
||||
reveal_type(x != "something else") # revealed: bool
|
||||
reveal_type("something else" != x) # revealed: bool
|
||||
|
||||
reveal_type(x == y) # revealed: bool
|
||||
reveal_type(y == x) # revealed: bool
|
||||
reveal_type(x != y) # revealed: bool
|
||||
reveal_type(y != x) # revealed: bool
|
||||
|
||||
reveal_type(x >= "abc") # revealed: bool
|
||||
reveal_type("abc" >= x) # revealed: bool
|
||||
|
||||
reveal_type(x in "abc") # revealed: bool
|
||||
reveal_type("abc" in x) # revealed: bool
|
||||
```
|
||||
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def get_int() -> int: ...
|
||||
|
||||
x = get_int()
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
o = object()
|
||||
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
### Unsupported operators for positive contributions
|
||||
|
||||
Raise an error if any of the positive contributions to the intersection type are unsupported for the
|
||||
given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
|
||||
Do *not* raise an error if any of the negative contributions to the intersection type are
|
||||
unsupported for the given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
@@ -58,9 +58,7 @@ reveal_type(c >= d) # revealed: Literal[True]
|
||||
#### Results with Ambiguity
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def bool_instance() -> bool: ...
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
@@ -136,158 +134,23 @@ reveal_type(c >= c) # revealed: Literal[True]
|
||||
|
||||
#### Non Boolean Rich Comparisons
|
||||
|
||||
Rich comparison methods defined in a class affect tuple comparisons as well. Proper type inference
|
||||
should be possible even in cases where these methods return non-boolean types.
|
||||
|
||||
Note: Tuples use lexicographic comparisons. If the `==` result for all paired elements in the tuple
|
||||
is True, the comparison then considers the tuple’s length. Regardless of the return type of the
|
||||
dunder methods, the final result can still be a boolean value.
|
||||
|
||||
(+cpython: For tuples, `==` and `!=` always produce boolean results, regardless of the return type
|
||||
of the dunder methods.)
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __eq__(self, o: object) -> str:
|
||||
return "hello"
|
||||
|
||||
def __ne__(self, o: object) -> bytes:
|
||||
return b"world"
|
||||
|
||||
def __lt__(self, o: A) -> float:
|
||||
return 3.14
|
||||
|
||||
def __le__(self, o: A) -> complex:
|
||||
return complex(0.5, -0.5)
|
||||
|
||||
def __gt__(self, o: A) -> tuple:
|
||||
return (1, 2, 3)
|
||||
|
||||
def __ge__(self, o: A) -> list:
|
||||
return [1, 2, 3]
|
||||
def __eq__(self, o) -> str: ...
|
||||
def __ne__(self, o) -> int: ...
|
||||
def __lt__(self, o) -> float: ...
|
||||
def __le__(self, o) -> object: ...
|
||||
def __gt__(self, o) -> tuple: ...
|
||||
def __ge__(self, o) -> list: ...
|
||||
|
||||
a = (A(), A())
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: float | Literal[False]
|
||||
reveal_type(a <= a) # revealed: complex | Literal[True]
|
||||
reveal_type(a > a) # revealed: tuple | Literal[False]
|
||||
reveal_type(a >= a) # revealed: list | Literal[True]
|
||||
|
||||
# If lexicographic comparison is finished before comparing A()
|
||||
b = ("1_foo", A())
|
||||
c = ("2_bar", A())
|
||||
|
||||
reveal_type(b == c) # revealed: Literal[False]
|
||||
reveal_type(b != c) # revealed: Literal[True]
|
||||
reveal_type(b < c) # revealed: Literal[True]
|
||||
reveal_type(b <= c) # revealed: Literal[True]
|
||||
reveal_type(b > c) # revealed: Literal[False]
|
||||
reveal_type(b >= c) # revealed: Literal[False]
|
||||
|
||||
class B:
|
||||
def __lt__(self, o: B) -> set:
|
||||
return set()
|
||||
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
|
||||
```
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
|
||||
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
|
||||
|
||||
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
|
||||
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
|
||||
immediately conclude that the tuples are not equal. Conversely, for `NotEq`, if any non-equal pair
|
||||
exists, we can determine that the tuples are unequal.
|
||||
|
||||
In contrast, with operators like `<` and `>`, the comparison must consider each pair of elements
|
||||
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
|
||||
|
||||
```py
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
|
||||
|
||||
a = (str_instance(), int_instance(), "foo")
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
b = (str_instance(), int_instance(), "bar")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
c = (str_instance(), int_instance(), "foo", "different_length")
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
```
|
||||
|
||||
#### Error Propagation
|
||||
|
||||
Errors occurring within a tuple comparison should propagate outward. However, if the tuple
|
||||
comparison can clearly conclude before encountering an error, the error should not be raised.
|
||||
|
||||
```py
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
|
||||
class A: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
|
||||
a = (0, int_instance(), A())
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, int_instance(), A())
|
||||
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Membership Test Comparisons
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
@@ -35,8 +33,4 @@ reveal_type(e) # revealed: bool
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (bool_instance(), A()) < (bool_instance(), A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -41,20 +41,21 @@ except EXCEPTIONS as f:
|
||||
## Dynamic exception types
|
||||
|
||||
```py
|
||||
# TODO: we should not emit these `call-possibly-unbound-method` errors for `tuple.__class_getitem__`
|
||||
def foo(
|
||||
x: type[AttributeError],
|
||||
y: tuple[type[OSError], type[RuntimeError]],
|
||||
z: tuple[type[BaseException], ...],
|
||||
y: tuple[type[OSError], type[RuntimeError]], # error: [call-possibly-unbound-method]
|
||||
z: tuple[type[BaseException], ...], # error: [call-possibly-unbound-method]
|
||||
):
|
||||
try:
|
||||
help()
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo(exception type)
|
||||
reveal_type(e) # revealed: @Todo
|
||||
except y as f:
|
||||
# TODO: should be `OSError | RuntimeError`
|
||||
reveal_type(f) # revealed: @Todo(exception type)
|
||||
reveal_type(f) # revealed: @Todo
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo(exception type)
|
||||
reveal_type(g) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Exception Handling
|
||||
|
||||
## Invalid syntax
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
# Attribute access
|
||||
|
||||
## Boundness
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class A:
|
||||
always_bound = 1
|
||||
|
||||
if flag():
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
|
||||
if flag():
|
||||
possibly_unbound = "abc"
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
# If expression
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
|
||||
```py
|
||||
reveal_type(1 if True else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if "not empty" else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if (1,) else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if 1 else 2) # revealed: Literal[1]
|
||||
|
||||
reveal_type(1 if False else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if None else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if "" else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Leaked Narrowing Constraint
|
||||
|
||||
(issue #14588)
|
||||
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
```
|
||||
@@ -1,219 +0,0 @@
|
||||
# Length (`len()`)
|
||||
|
||||
## Literal and constructed iterables
|
||||
|
||||
### Strings and bytes literals
|
||||
|
||||
```py
|
||||
reveal_type(len("no\rmal")) # revealed: Literal[6]
|
||||
reveal_type(len(r"aw stri\ng")) # revealed: Literal[10]
|
||||
reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14]
|
||||
reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11]
|
||||
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7]
|
||||
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
r"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
b"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
rb"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Tuples
|
||||
|
||||
```py
|
||||
reveal_type(len(())) # revealed: Literal[0]
|
||||
reveal_type(len((1,))) # revealed: Literal[1]
|
||||
reveal_type(len((1, 2))) # revealed: Literal[2]
|
||||
|
||||
# TODO: Handle constructor calls
|
||||
reveal_type(len(tuple())) # revealed: int
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[1]
|
||||
reveal_type( # revealed: Literal[2]
|
||||
len(
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Lists, sets and dictionaries
|
||||
|
||||
```py
|
||||
reveal_type(len([])) # revealed: int
|
||||
reveal_type(len([1])) # revealed: int
|
||||
reveal_type(len([1, 2])) # revealed: int
|
||||
reveal_type(len([*{}, *dict()])) # revealed: int
|
||||
|
||||
reveal_type(len({})) # revealed: int
|
||||
reveal_type(len({**{}})) # revealed: int
|
||||
reveal_type(len({**{}, **{}})) # revealed: int
|
||||
|
||||
reveal_type(len({1})) # revealed: int
|
||||
reveal_type(len({1, 2})) # revealed: int
|
||||
reveal_type(len({*[], 2})) # revealed: int
|
||||
|
||||
reveal_type(len(list())) # revealed: int
|
||||
reveal_type(len(set())) # revealed: int
|
||||
reveal_type(len(dict())) # revealed: int
|
||||
reveal_type(len(frozenset())) # revealed: int
|
||||
```
|
||||
|
||||
## `__len__`
|
||||
|
||||
The returned value of `__len__` is implicitly and recursively converted to `int`.
|
||||
|
||||
### Literal integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Zero:
|
||||
def __len__(self) -> Literal[0]: ...
|
||||
|
||||
class ZeroOrOne:
|
||||
def __len__(self) -> Literal[0, 1]: ...
|
||||
|
||||
class ZeroOrTrue:
|
||||
def __len__(self) -> Literal[0, True]: ...
|
||||
|
||||
class OneOrFalse:
|
||||
def __len__(self) -> Literal[1] | Literal[False]: ...
|
||||
|
||||
class OneOrFoo:
|
||||
def __len__(self) -> Literal[1, "foo"]: ...
|
||||
|
||||
class ZeroOrStr:
|
||||
def __len__(self) -> Literal[0] | str: ...
|
||||
|
||||
reveal_type(len(Zero())) # revealed: Literal[0]
|
||||
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(OneOrFoo())) # revealed: int
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(ZeroOrStr())) # revealed: int
|
||||
```
|
||||
|
||||
### Literal booleans
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class LiteralTrue:
|
||||
def __len__(self) -> Literal[True]: ...
|
||||
|
||||
class LiteralFalse:
|
||||
def __len__(self) -> Literal[False]: ...
|
||||
|
||||
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
|
||||
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```py
|
||||
from enum import Enum, auto
|
||||
from typing import Literal
|
||||
|
||||
class SomeEnum(Enum):
|
||||
AUTO = auto()
|
||||
INT = 2
|
||||
STR = "4"
|
||||
TUPLE = (8, "16")
|
||||
INT_2 = 3_2
|
||||
|
||||
class Auto:
|
||||
def __len__(self) -> Literal[SomeEnum.AUTO]: ...
|
||||
|
||||
class Int:
|
||||
def __len__(self) -> Literal[SomeEnum.INT]: ...
|
||||
|
||||
class Str:
|
||||
def __len__(self) -> Literal[SomeEnum.STR]: ...
|
||||
|
||||
class Tuple:
|
||||
def __len__(self) -> Literal[SomeEnum.TUPLE]: ...
|
||||
|
||||
class IntUnion:
|
||||
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
|
||||
|
||||
reveal_type(len(Auto())) # revealed: int
|
||||
reveal_type(len(Int())) # revealed: Literal[2]
|
||||
reveal_type(len(Str())) # revealed: int
|
||||
reveal_type(len(Tuple())) # revealed: int
|
||||
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
|
||||
```
|
||||
|
||||
### Negative integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Negative:
|
||||
def __len__(self) -> Literal[-1]: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(Negative())) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class SecondOptionalArgument:
|
||||
def __len__(self, v: int = 0) -> Literal[0]: ...
|
||||
|
||||
class SecondRequiredArgument:
|
||||
def __len__(self, v: int) -> Literal[1]: ...
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### No `__len__`
|
||||
|
||||
```py
|
||||
class NoDunderLen:
|
||||
pass
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
```
|
||||
@@ -1,75 +0,0 @@
|
||||
# Function parameter types
|
||||
|
||||
Within a function scope, the declared type of each parameter is its annotated type (or Unknown if
|
||||
not annotated). The initial inferred type is the union of the declared type with the type of the
|
||||
default value expression (if any). If both are fully static types, this union should simplify to the
|
||||
annotated type (since the default value type must be assignable to the annotated type, and for fully
|
||||
static types this means subtype-of, which simplifies in unions). But if the annotated type is
|
||||
Unknown or another non-fully-static type, the default value type may still be relevant as lower
|
||||
bound.
|
||||
|
||||
The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is
|
||||
a dictionary from strings to its annotated type.
|
||||
|
||||
## Parameter kinds
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: int
|
||||
reveal_type(c) # revealed: Unknown | Literal[1]
|
||||
reveal_type(d) # revealed: int
|
||||
reveal_type(e) # revealed: Unknown | Literal[3]
|
||||
reveal_type(f) # revealed: Literal[4]
|
||||
reveal_type(g) # revealed: Unknown | Literal[5]
|
||||
reveal_type(h) # revealed: Literal[6]
|
||||
|
||||
# TODO: should be `tuple[object, ...]` (needs generics)
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
# TODO: should be `dict[str, str]` (needs generics)
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
```
|
||||
|
||||
## Unannotated variadic parameters
|
||||
|
||||
...are inferred as tuple of Unknown or dict from string to Unknown.
|
||||
|
||||
```py
|
||||
def g(*args, **kwargs):
|
||||
# TODO: should be `tuple[Unknown, ...]` (needs generics)
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
# TODO: should be `dict[str, Unknown]` (needs generics)
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
```
|
||||
|
||||
## Annotation is present but not a fully static type
|
||||
|
||||
The default value type should be a lower bound on the inferred type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any = 1):
|
||||
reveal_type(x) # revealed: Any | Literal[1]
|
||||
```
|
||||
|
||||
## Default value type must be assignable to annotated type
|
||||
|
||||
The default value type must be assignable to the annotated type. If not, we emit a diagnostic, and
|
||||
fall back to inferring the annotated type, ignoring the default value type.
|
||||
|
||||
```py
|
||||
# error: [invalid-parameter-default]
|
||||
def f(x: int = "foo"):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# The check is assignable-to, not subtype-of, so this is fine:
|
||||
from typing import Any
|
||||
|
||||
def g(x: Any = "foo"):
|
||||
reveal_type(x) # revealed: Any | Literal["foo"]
|
||||
```
|
||||
@@ -6,9 +6,13 @@ Basic PEP 695 generics
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
box_model_number = 695
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
@@ -18,7 +22,7 @@ box: MyBox[int] = MyBox(5)
|
||||
wrong_innards: MyBox[int] = MyBox("five")
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(box.data) # revealed: @Todo(instance attributes)
|
||||
reveal_type(box.data) # revealed: @Todo
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
```
|
||||
@@ -27,19 +31,24 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
data: T
|
||||
|
||||
# TODO: `T` is defined here
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error on the subscripting
|
||||
# TODO not error on the subscripting or the use of type param
|
||||
# error: [unresolved-reference] "Name `T` used when not defined"
|
||||
# error: [non-subscriptable]
|
||||
class MySecureBox[T](MyBox[T]): ...
|
||||
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
# TODO reveal int
|
||||
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
|
||||
reveal_type(secure_box.data) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Cyclical class definition
|
||||
@@ -57,23 +66,3 @@ class S[T](Seq[S]): ... # error: [non-subscriptable]
|
||||
|
||||
reveal_type(S) # revealed: Literal[S]
|
||||
```
|
||||
|
||||
## Type params
|
||||
|
||||
A PEP695 type variable defines a value of type `typing.TypeVar`.
|
||||
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(T) # revealed: T
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
## Minimum two constraints
|
||||
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -21,7 +21,6 @@ reveal_type(y)
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
|
||||
from maybe_unbound import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
@@ -51,7 +50,6 @@ reveal_type(y)
|
||||
Importing an annotated name prefers the declared type over the inferred type:
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
|
||||
from maybe_unbound_annotated import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
|
||||
@@ -55,24 +55,3 @@ from b import x
|
||||
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
||||
```
|
||||
|
||||
## Import cycle
|
||||
|
||||
```py path=a.py
|
||||
class A: ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
|
||||
import b
|
||||
|
||||
class C(b.B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
from a import A
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Invalid syntax
|
||||
|
||||
## Missing module name
|
||||
|
||||
```py
|
||||
from import bar # error: [invalid-syntax]
|
||||
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
@@ -1,93 +0,0 @@
|
||||
# Syntax errors
|
||||
|
||||
Test cases to ensure that red knot does not panic if there are syntax errors in the source code.
|
||||
|
||||
The parser cannot recover from certain syntax errors completely which is why the number of syntax
|
||||
errors could be more than expected in the following examples. For instance, if there's a keyword
|
||||
(like `for`) in the middle of another statement (like function definition), then it's more likely
|
||||
that the rest of the tokens are going to be part of the `for` statement and not the function
|
||||
definition. But, it's not necessary that the remaining tokens are valid in the context of a `for`
|
||||
statement.
|
||||
|
||||
## Keyword as identifiers
|
||||
|
||||
When keywords are used as identifiers, the parser recovers from this syntax error by emitting an
|
||||
error and including the text value of the keyword to create the `Identifier` node.
|
||||
|
||||
### Name expression
|
||||
|
||||
#### Assignment
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
pass = 1
|
||||
```
|
||||
|
||||
#### Type alias
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
type pass = 1
|
||||
```
|
||||
|
||||
#### Function definition
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
def True(for):
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
#### For
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `pass` used when not defined"
|
||||
for while in pass:
|
||||
pass
|
||||
```
|
||||
|
||||
#### While
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `in` used when not defined"
|
||||
while in:
|
||||
pass
|
||||
```
|
||||
|
||||
#### Match
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `match` used when not defined"
|
||||
match while:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `case` used when not defined"
|
||||
case in:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
### Attribute expression
|
||||
|
||||
```py
|
||||
# TODO: Check when support for attribute expressions is added
|
||||
|
||||
# error: [invalid-syntax]
|
||||
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
for x in foo.pass:
|
||||
pass
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
# Ellipsis literals
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
reveal_type(...) # revealed: EllipsisType | ellipsis
|
||||
```
|
||||
@@ -1,93 +0,0 @@
|
||||
# Literal
|
||||
|
||||
<https://typing.readthedocs.io/en/latest/spec/literal.html#literals>
|
||||
|
||||
## Parameterization
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
mode: Literal["w", "r"]
|
||||
mode2: Literal["w"] | Literal["r"]
|
||||
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
|
||||
a1: Literal[26]
|
||||
a2: Literal[0x1A]
|
||||
a3: Literal[-4]
|
||||
a4: Literal["hello world"]
|
||||
a5: Literal[b"hello world"]
|
||||
a6: Literal[True]
|
||||
a7: Literal[None]
|
||||
a8: Literal[Literal[1]]
|
||||
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
GREEN = 1
|
||||
BLUE = 2
|
||||
|
||||
b1: Literal[Color.RED]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(mode2) # revealed: Literal["w", "r"]
|
||||
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
reveal_type(a2) # revealed: Literal[26]
|
||||
reveal_type(a3) # revealed: Literal[-4]
|
||||
reveal_type(a4) # revealed: Literal["hello world"]
|
||||
reveal_type(a5) # revealed: Literal[b"hello world"]
|
||||
reveal_type(a6) # revealed: Literal[True]
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid3: Literal[(3, 4)]
|
||||
|
||||
hello = "hello"
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-literal-parameter]
|
||||
"foo",
|
||||
hello, # error: [invalid-literal-parameter]
|
||||
(1, 2, 3), # error: [invalid-literal-parameter]
|
||||
]
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
Literal.
|
||||
|
||||
```pyi path=other.pyi
|
||||
from typing import _SpecialForm
|
||||
|
||||
Literal: _SpecialForm
|
||||
```
|
||||
|
||||
```py
|
||||
from other import Literal
|
||||
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: @Todo(generics)
|
||||
```
|
||||
|
||||
## Detecting typing_extensions.Literal
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
```
|
||||
@@ -18,7 +18,7 @@ async def foo():
|
||||
pass
|
||||
|
||||
# TODO: should reveal `Unknown` because `__aiter__` is not defined
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
# revealed: @Todo
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
@@ -40,6 +40,6 @@ async def foo():
|
||||
pass
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
# revealed: @Todo
|
||||
reveal_type(x)
|
||||
```
|
||||
@@ -238,7 +238,7 @@ class Test:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
# TODO: we should emit a diagnostic here (it might not be iterable)
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
@@ -52,29 +52,3 @@ else:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## Nested while loops
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
x = 1
|
||||
|
||||
while flag():
|
||||
x = 2
|
||||
|
||||
while flag():
|
||||
x = 3
|
||||
if flag():
|
||||
break
|
||||
else:
|
||||
x = 4
|
||||
|
||||
if flag():
|
||||
break
|
||||
else:
|
||||
x = 5
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
```
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
This test makes sure that `red_knot_test` correctly parses the TOML configuration blocks and applies
|
||||
the correct settings hierarchically.
|
||||
|
||||
The following configuration will be attached to the *root* section (without any heading):
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.10"
|
||||
```
|
||||
|
||||
# Basic
|
||||
|
||||
Here, we simply make sure that we pick up the global configuration from the root section:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# Inheritance
|
||||
|
||||
## Child
|
||||
|
||||
### Grandchild
|
||||
|
||||
The same should work for arbitrarily nested sections:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# Overwriting
|
||||
|
||||
Here, we make sure that we can overwrite the global configuration in a child section:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# No global state
|
||||
|
||||
There is no global state. This section should again use the root configuration:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
# Overwriting affects children
|
||||
|
||||
Children in this section should all use the section configuration:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.12"
|
||||
```
|
||||
|
||||
## Child
|
||||
|
||||
### Grandchild
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info[:2] == (3, 12)) # revealed: Literal[True]
|
||||
```
|
||||
@@ -1,196 +0,0 @@
|
||||
## Default
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
|
||||
reveal_type(M.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `object`
|
||||
|
||||
```py
|
||||
reveal_type(object.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `type`
|
||||
|
||||
```py
|
||||
reveal_type(type.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class B(metaclass=M): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Invalid metaclass
|
||||
|
||||
A class which doesn't inherit `type` (and/or doesn't implement a custom `__new__` accepting the same
|
||||
arguments as `type.__new__`) isn't a valid metaclass.
|
||||
|
||||
```py
|
||||
class M: ...
|
||||
class A(metaclass=M): ...
|
||||
|
||||
# TODO: emit a diagnostic for the invalid metaclass
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Linear inheritance
|
||||
|
||||
If a class is a subclass of a class with a custom metaclass, then the subclass will also have that
|
||||
metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Conflict (1)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Conflict (2)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
|
||||
class B(A, metaclass=M2): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Common metaclass
|
||||
|
||||
A class has two explicit bases, both of which have the same metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(metaclass=M): ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Metaclass metaclass
|
||||
|
||||
A class has an explicit base with a custom metaclass. That metaclass itself has a custom metaclass.
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type, metaclass=M1): ...
|
||||
class M3(M2): ...
|
||||
class A(metaclass=M3): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M3]
|
||||
```
|
||||
|
||||
## Diamond inheritance
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class M1(M): ...
|
||||
class M2(M): ...
|
||||
class M12(M1, M2): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
class C(metaclass=M12): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class D(A, B, C): ...
|
||||
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown
|
||||
|
||||
```py
|
||||
from nonexistent_module import UnknownClass # error: [unresolved-import]
|
||||
|
||||
class C(UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[type] & Unknown`
|
||||
reveal_type(C.__class__) # revealed: Literal[type]
|
||||
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[M] & Unknown`
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Duplicate
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Non-class
|
||||
|
||||
When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
|
||||
`type.__new__` arguments, we should return the meta type of its return type.
|
||||
|
||||
```py
|
||||
def f(*args, **kwargs) -> int: ...
|
||||
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
@@ -1,409 +0,0 @@
|
||||
# Method Resolution Order tests
|
||||
|
||||
Tests that assert that we can infer the correct type for a class's `__mro__` attribute.
|
||||
|
||||
This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to
|
||||
know the precise possible values of a class's Method Resolution Order, or we won't be able to infer
|
||||
the correct type of attributes accessed from instances.
|
||||
|
||||
For documentation on method resolution orders, see:
|
||||
|
||||
- <https://docs.python.org/3/glossary.html#term-method-resolution-order>
|
||||
- <https://docs.python.org/3/howto/mro.html#python-2-3-mro>
|
||||
|
||||
## No bases
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
```
|
||||
|
||||
## The special case: `object` itself
|
||||
|
||||
```py
|
||||
reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
|
||||
```
|
||||
|
||||
## Explicit inheritance from `object`
|
||||
|
||||
```py
|
||||
class C(object): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
```
|
||||
|
||||
## Explicit inheritance from non-`object` single base
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
## Linearization of multiple bases
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (1)
|
||||
|
||||
This is "ex_2" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (2)
|
||||
|
||||
This is "ex_5" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class F(O): ...
|
||||
class E(O): ...
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(D, E): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (3)
|
||||
|
||||
This is "ex_6" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class F(O): ...
|
||||
class E(O): ...
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (4)
|
||||
|
||||
This is "ex_9" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class A(O): ...
|
||||
class B(O): ...
|
||||
class C(O): ...
|
||||
class D(O): ...
|
||||
class E(O): ...
|
||||
class K1(A, B, C): ...
|
||||
class K2(D, B, E): ...
|
||||
class K3(D, A): ...
|
||||
class Z(K1, K2, K3): ...
|
||||
|
||||
# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]]
|
||||
reveal_type(K1.__mro__)
|
||||
# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(K2.__mro__)
|
||||
# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]]
|
||||
reveal_type(K3.__mro__)
|
||||
# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(Z.__mro__)
|
||||
```
|
||||
|
||||
## Inheritance from `Unknown`
|
||||
|
||||
```py
|
||||
from does_not_exist import DoesNotExist # error: [unresolved-import]
|
||||
|
||||
class A(DoesNotExist): ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D(A, B, C): ...
|
||||
class E(B, C): ...
|
||||
class F(E, A): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors at runtime
|
||||
|
||||
If the class's `__bases__` cause an exception to be raised at runtime and therefore the class
|
||||
creation to fail, we infer the class's `__mro__` as being `[<class>, Unknown, object]`:
|
||||
|
||||
```py
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[<class 'object'>, <class 'int'>]`"
|
||||
class Foo(object, int): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar(Foo): ...
|
||||
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
# This is the `TypeError` at the bottom of "ex_2"
|
||||
# in the examples at <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[<class 'A'>, <class 'B'>]`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
|
||||
class AA(Z): ...
|
||||
|
||||
reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` includes a `Union`
|
||||
|
||||
We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`.
|
||||
If we find a union type in a class's bases, we infer the class's `__mro__` as being
|
||||
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
if returns_bool():
|
||||
x = A
|
||||
else:
|
||||
x = B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` includes multiple `Union`s
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
|
||||
if returns_bool():
|
||||
x = A
|
||||
else:
|
||||
x = B
|
||||
|
||||
if returns_bool():
|
||||
y = C
|
||||
else:
|
||||
y = D
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(y) # revealed: Literal[C, D]
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x, y): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors... now with `Union`s
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
|
||||
if returns_bool():
|
||||
foo = Y
|
||||
else:
|
||||
foo = object
|
||||
|
||||
# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class PossibleError(foo, X): ...
|
||||
|
||||
reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]]
|
||||
|
||||
class A(X, Y): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
|
||||
if returns_bool():
|
||||
class B(X, Y): ...
|
||||
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
|
||||
# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate bases
|
||||
|
||||
```py
|
||||
class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`"
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Spam: ...
|
||||
class Eggs: ...
|
||||
class Ham(
|
||||
Spam,
|
||||
Eggs,
|
||||
Spam, # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
): ...
|
||||
|
||||
reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]]
|
||||
|
||||
class Mushrooms: ...
|
||||
class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
|
||||
reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate `Unknown` bases
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
# error: [unresolved-import]
|
||||
from does_not_exist import unknown_object_1, unknown_object_2
|
||||
|
||||
reveal_type(unknown_object_1) # revealed: Unknown
|
||||
reveal_type(unknown_object_2) # revealed: Unknown
|
||||
|
||||
# We *should* emit an error here to warn the user that we have no idea
|
||||
# what the MRO of this class should really be.
|
||||
# However, we don't complain about "duplicate base classes" here,
|
||||
# even though two classes are both inferred as being `Unknown`.
|
||||
#
|
||||
# (TODO: should we revisit this? Does it violate the gradual guarantee?
|
||||
# Should we just silently infer `[Foo, Unknown, object]` as the MRO here
|
||||
# without emitting any error at all? Not sure...)
|
||||
#
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
|
||||
class Foo(unknown_object_1, unknown_object_2): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
|
||||
|
||||
```py
|
||||
from does_not_exist import unknown_object # error: [unresolved-import]
|
||||
|
||||
reveal_type(unknown_object) # revealed: Unknown
|
||||
reveal_type(unknown_object.__mro__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Classes that inherit from themselves
|
||||
|
||||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with indirect cycles in their MROs
|
||||
|
||||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MROs, and multiple inheritance
|
||||
|
||||
```py path=a.pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]]
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
## Narrowing for `bool(..)` checks
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
x = 1 if flag() else None
|
||||
|
||||
# valid invocation, positive
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# valid invocation, negative
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool(x is not None):
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
# no args/narrowing
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool():
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
|
||||
# invalid invocation, too many positional args
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, 5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
|
||||
# invalid invocation, too many kwargs
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, y=5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
@@ -1,282 +0,0 @@
|
||||
# Narrowing for conditionals with boolean expressions
|
||||
|
||||
## Narrowing in `and` conditional
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
```
|
||||
|
||||
## Arms might not add narrowing constraints
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and bool_instance():
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if bool_instance() and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## Statically known arms
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## The type of multiple symbols can be narrowed down
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
```
|
||||
|
||||
## Narrowing in `or` conditional
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## In `or`, all arms should add constraint in order to narrow
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B) or bool_instance():
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## in `or`, all arms should narrow the same set of symbols
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
```
|
||||
|
||||
## mixing `and` and `not`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
```
|
||||
|
||||
## mixing `or` and `not`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
```
|
||||
|
||||
## `or` with nested `and`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
```
|
||||
|
||||
## `and` with nested `or`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
```
|
||||
|
||||
## Boolean expression internal narrowing
|
||||
|
||||
```py
|
||||
def optional_string() -> str | None:
|
||||
return None
|
||||
|
||||
x = optional_string()
|
||||
y = optional_string()
|
||||
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
```
|
||||
@@ -1,247 +0,0 @@
|
||||
# Narrowing for `issubclass` checks
|
||||
|
||||
Narrowing for `issubclass(class, classinfo)` expressions.
|
||||
|
||||
## `classinfo` is a single type
|
||||
|
||||
### Basic example
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Proper narrowing in `elif` and `else` branches
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
class Derived1(Base): ...
|
||||
class Derived2(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t1 = Derived1 if flag() else Derived2
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
|
||||
t2 = Derived1 if flag() else Base
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
|
||||
t3 = Derived1 if flag() else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_class() -> type[object]: ...
|
||||
|
||||
t = get_class()
|
||||
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
|
||||
```py
|
||||
# TODO: this error should ideally go away once we (1) understand `sys.version_info` branches,
|
||||
# and (2) set the target Python version for this test to 3.10.
|
||||
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
|
||||
from types import NoneType
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else NoneType
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
|
||||
### (Nested) tuples of types
|
||||
|
||||
```py
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Special cases
|
||||
|
||||
### Emit a diagnostic if the first argument is of wrong type
|
||||
|
||||
#### Too wide
|
||||
|
||||
`type[object]` is a subtype of `object`, but not every `object` can be passed as the first argument
|
||||
to `issubclass`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
t = get_object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
```
|
||||
|
||||
#### Wrong
|
||||
|
||||
`Literal[1]` and `type` are entirely disjoint, so the inferred type of `Literal[1] & type[int]` is
|
||||
eagerly simplified to `Never` as a result of the type narrowing in the `if issubclass(t, int)`
|
||||
branch:
|
||||
|
||||
```py
|
||||
t = 1
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Do not use custom `issubclass` for narrowing
|
||||
|
||||
```py
|
||||
def issubclass(c, ci):
|
||||
return True
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is aliased
|
||||
|
||||
```py
|
||||
issubclass_alias = issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass_alias(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is imported
|
||||
|
||||
```py
|
||||
from builtins import issubclass as imported_issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if imported_issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do not narrow if second argument is not a proper `classinfo` argument
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, "str"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, (bytes, "str")):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, Any):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`issubclass` has no `foo` parameter)
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
@@ -1,64 +0,0 @@
|
||||
# Consolidating narrowed types after if statement
|
||||
|
||||
## After if-else statements, narrowing has no effect if the variable is not mutated in any branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Narrowing can have a persistent effect if the variable is mutated in one branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 10
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## An if statement without an explicit `else` branch is equivalent to one with a no-op `else` branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
x = optional_int()
|
||||
y = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 0
|
||||
|
||||
if y is None:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: int | None
|
||||
```
|
||||
|
||||
## An if-elif without an explicit else branch is equivalent to one with an empty else branch
|
||||
|
||||
```py
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 0
|
||||
elif x > 50:
|
||||
x = 50
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
@@ -1,152 +0,0 @@
|
||||
# Narrowing for checks involving `type(x)`
|
||||
|
||||
## `type(x) is C`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
# It would be wrong to infer `B` here. The type
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## `type(x) is not C`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## `type(x) == C`, `type(x) != C`
|
||||
|
||||
No narrowing can occur for equality comparisons, since there might be a custom `__eq__`
|
||||
implementation on the metaclass.
|
||||
|
||||
TODO: Narrowing might be possible in some cases where the classes themselves are `@final` or their
|
||||
metaclass is `@final`.
|
||||
|
||||
```py
|
||||
class IsEqualToEverything(type):
|
||||
def __eq__(cls, other):
|
||||
return True
|
||||
|
||||
class A(metaclass=IsEqualToEverything): ...
|
||||
class B(metaclass=IsEqualToEverything): ...
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return B()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for custom `type` callable
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def type(x):
|
||||
return int
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for multiple arguments
|
||||
|
||||
No narrowing should occur if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## No narrowing for keyword arguments
|
||||
|
||||
`type` can't be used with a keyword argument:
|
||||
|
||||
```py
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
# TODO: we could issue a diagnostic here
|
||||
if type(object=x) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## Narrowing if `type` is aliased
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
alias_for_type = type
|
||||
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
|
||||
def get_base() -> Base:
|
||||
return Base()
|
||||
|
||||
x = get_base()
|
||||
|
||||
if type(x) is Base:
|
||||
# Ideally, this could be narrower, but there is now way to
|
||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||
reveal_type(x) # revealed: Base
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
# Regression test for #14334
|
||||
|
||||
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
|
||||
|
||||
```py path=base.py
|
||||
# error: [invalid-base]
|
||||
class Base(2): ...
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
# No error here
|
||||
from base import Base
|
||||
```
|
||||
@@ -17,7 +17,8 @@ reveal_type(__doc__) # revealed: str | None
|
||||
# (needs support for `*` imports)
|
||||
reveal_type(__spec__) # revealed: Unknown | None
|
||||
|
||||
reveal_type(__path__) # revealed: @Todo(generics)
|
||||
# TODO: generics
|
||||
reveal_type(__path__) # revealed: @Todo
|
||||
|
||||
class X:
|
||||
reveal_type(__name__) # revealed: str
|
||||
@@ -57,13 +58,15 @@ reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
# TODO: we don't currently understand `types.ModuleType` as inheriting from `object`;
|
||||
# these should not reveal `Unknown`:
|
||||
reveal_type(typing.__eq__) # revealed: Unknown
|
||||
reveal_type(typing.__class__) # revealed: Unknown
|
||||
reveal_type(typing.__module__) # revealed: Unknown
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(typing.__dict__) # revealed: @Todo
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
@@ -73,7 +76,6 @@ we're dealing with:
|
||||
```py path=__getattr__.py
|
||||
import typing
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -95,8 +97,8 @@ from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties, and generics;
|
||||
# should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
|
||||
reveal_type(foo.__dict__) # revealed: @Todo
|
||||
reveal_type(foo_dict) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Nonlocal references
|
||||
|
||||
## One level up
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Two levels up
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Skips class scope
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
x = 2
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Skips annotation-only assignment
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
# it's pretty weird to have an annotated assignment in a function where the
|
||||
# name is otherwise not defined; maybe should be an error?
|
||||
x: int
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
@@ -1,57 +0,0 @@
|
||||
# Unbound
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
y = x
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
```
|
||||
|
||||
## Unbound function local
|
||||
|
||||
An unbound function local that has definitions in the scope does not fall back to globals.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
def f():
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
reveal_type(x)
|
||||
x = 2
|
||||
# revealed: Literal[2]
|
||||
reveal_type(x)
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# Class definitions in stubs
|
||||
# Class defenitions in stubs
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
@@ -6,12 +6,7 @@ In type stubs, classes can reference themselves in their base class definitions.
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo[T]: ...
|
||||
class C(C): ...
|
||||
|
||||
# TODO: actually is subscriptable
|
||||
# error: [non-subscriptable]
|
||||
class Bar(Foo[Bar]): ...
|
||||
|
||||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@ def int_instance() -> int:
|
||||
|
||||
a = b"abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
reveal_type(a) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -47,11 +47,11 @@ def int_instance() -> int: ...
|
||||
|
||||
byte_slice1 = b[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
reveal_type(byte_slice1) # revealed: @Todo
|
||||
|
||||
def bytes_instance() -> bytes: ...
|
||||
|
||||
byte_slice2 = bytes_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
reveal_type(byte_slice2) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -21,11 +21,10 @@ reveal_type(Identity[0]) # revealed: str
|
||||
## Class getitem union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
flag = True
|
||||
|
||||
class UnionClassGetItem:
|
||||
if bool_instance():
|
||||
if flag:
|
||||
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
@@ -40,8 +39,7 @@ reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
## Class getitem with class union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
flag = True
|
||||
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
@@ -51,7 +49,7 @@ class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
x = A if bool_instance() else B
|
||||
x = A if flag else B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
@@ -60,10 +58,9 @@ reveal_type(x[0]) # revealed: str | int
|
||||
## Class getitem with unbound method union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
flag = True
|
||||
|
||||
if bool_instance():
|
||||
if flag:
|
||||
class Spam:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
@@ -79,10 +76,9 @@ reveal_type(Spam[42])
|
||||
## TODO: Class getitem non-class union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
flag = True
|
||||
|
||||
if bool_instance():
|
||||
if flag:
|
||||
class Eggs:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
@@ -30,11 +30,10 @@ reveal_type(Identity()[0]) # revealed: int
|
||||
## Getitem union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
flag = True
|
||||
|
||||
class Identity:
|
||||
if bool_instance():
|
||||
if flag:
|
||||
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
|
||||
@@ -12,13 +12,13 @@ x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo(return type)
|
||||
reveal_type(x[0]) # revealed: @Todo
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type)
|
||||
reveal_type(x[0:1]) # revealed: @Todo
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type)
|
||||
reveal_type(x["a"]) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Assignments within list assignment
|
||||
|
||||
@@ -23,7 +23,7 @@ def int_instance() -> int: ...
|
||||
|
||||
a = "abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
reveal_type(a) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -78,13 +78,13 @@ def int_instance() -> int: ...
|
||||
|
||||
substring1 = s[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
reveal_type(substring1) # revealed: @Todo
|
||||
|
||||
def str_instance() -> str: ...
|
||||
|
||||
substring2 = str_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
reveal_type(substring2) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
@@ -71,5 +71,5 @@ def int_instance() -> int: ...
|
||||
|
||||
tuple_slice = t[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
reveal_type(tuple_slice) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
# `sys.version_info`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
target-version = "3.9"
|
||||
```
|
||||
|
||||
## The type of `sys.version_info`
|
||||
|
||||
The type of `sys.version_info` is `sys._version_info`, at least according to typeshed's stubs (which
|
||||
we treat as the single source of truth for the standard library). This is quite a complicated type
|
||||
in typeshed, so there are many things we don't fully understand about the type yet; this is the
|
||||
source of several TODOs in this test file. Many of these TODOs should be naturally fixed as we
|
||||
implement more type-system features in the future.
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info) # revealed: _version_info
|
||||
```
|
||||
|
||||
## Literal types from comparisons
|
||||
|
||||
Comparing `sys.version_info` with a 2-element tuple of literal integers always produces a `Literal`
|
||||
type:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info >= (3, 9)) # revealed: Literal[True]
|
||||
reveal_type((3, 9) <= sys.version_info) # revealed: Literal[True]
|
||||
|
||||
reveal_type(sys.version_info > (3, 9)) # revealed: Literal[True]
|
||||
reveal_type((3, 9) < sys.version_info) # revealed: Literal[True]
|
||||
|
||||
reveal_type(sys.version_info < (3, 9)) # revealed: Literal[False]
|
||||
reveal_type((3, 9) > sys.version_info) # revealed: Literal[False]
|
||||
|
||||
reveal_type(sys.version_info <= (3, 9)) # revealed: Literal[False]
|
||||
reveal_type((3, 9) >= sys.version_info) # revealed: Literal[False]
|
||||
|
||||
reveal_type(sys.version_info == (3, 9)) # revealed: Literal[False]
|
||||
reveal_type((3, 9) == sys.version_info) # revealed: Literal[False]
|
||||
|
||||
reveal_type(sys.version_info != (3, 9)) # revealed: Literal[True]
|
||||
reveal_type((3, 9) != sys.version_info) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Non-literal types from comparisons
|
||||
|
||||
Comparing `sys.version_info` with tuples of other lengths will sometimes produce `Literal` types,
|
||||
sometimes not:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: bool
|
||||
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: bool
|
||||
|
||||
# TODO: While this won't fail at runtime, the user has probably made a mistake
|
||||
# if they're comparing a tuple of length >5 with `sys.version_info`
|
||||
# (`sys.version_info` is a tuple of length 5). It might be worth
|
||||
# emitting a lint diagnostic of some kind warning them about the probable error?
|
||||
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: bool
|
||||
|
||||
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Imports and aliases
|
||||
|
||||
Comparisons with `sys.version_info` still produce literal types, even if the symbol is aliased to
|
||||
another name:
|
||||
|
||||
```py
|
||||
from sys import version_info
|
||||
from sys import version_info as foo
|
||||
|
||||
reveal_type(version_info >= (3, 9)) # revealed: Literal[True]
|
||||
reveal_type(foo >= (3, 9)) # revealed: Literal[True]
|
||||
|
||||
bar = version_info
|
||||
reveal_type(bar >= (3, 9)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Non-stdlib modules named `sys`
|
||||
|
||||
Only comparisons with the symbol `version_info` from the `sys` module produce literal types:
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/sys.py
|
||||
version_info: tuple[int, int] = (4, 2)
|
||||
```
|
||||
|
||||
```py path=package/script.py
|
||||
from .sys import version_info
|
||||
|
||||
reveal_type(version_info >= (3, 9)) # revealed: bool
|
||||
```
|
||||
|
||||
## Accessing fields by name
|
||||
|
||||
The fields of `sys.version_info` can be accessed by name:
|
||||
|
||||
```py path=a.py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
|
||||
reveal_type(sys.version_info.minor >= 9) # revealed: Literal[True]
|
||||
reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
|
||||
properties on instance types:
|
||||
|
||||
```py path=b.py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Accessing fields by index/slice
|
||||
|
||||
The fields of `sys.version_info` can be accessed by index or by slice:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[0] < 3) # revealed: Literal[False]
|
||||
reveal_type(sys.version_info[1] > 9) # revealed: Literal[False]
|
||||
|
||||
# revealed: tuple[Literal[3], Literal[9], int, Literal["alpha", "beta", "candidate", "final"], int]
|
||||
reveal_type(sys.version_info[:5])
|
||||
|
||||
reveal_type(sys.version_info[:2] >= (3, 9)) # revealed: Literal[True]
|
||||
reveal_type(sys.version_info[0:2] >= (3, 10)) # revealed: Literal[False]
|
||||
reveal_type(sys.version_info[:3] >= (3, 10, 1)) # revealed: Literal[False]
|
||||
reveal_type(sys.version_info[3] == "final") # revealed: bool
|
||||
reveal_type(sys.version_info[3] == "finalllllll") # revealed: Literal[False]
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
# Type aliases
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
|
||||
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
|
||||
|
||||
x: IntOrStr = 1
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## `__value__` attribute
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
# TODO: This should either fall back to the specified type from typeshed,
|
||||
# which is `Any`, or be the actual type of the runtime value expression
|
||||
# `int | str`, i.e. `types.UnionType`.
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
|
||||
```py
|
||||
type OptionalInt = int | None
|
||||
|
||||
# error: [invalid-assignment]
|
||||
x: OptionalInt = "1"
|
||||
```
|
||||
|
||||
## Type aliases in type aliases
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
type IntOrStrOrBytes = IntOrStr | bytes
|
||||
|
||||
x: IntOrStrOrBytes = 1
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: int | str | bytes
|
||||
```
|
||||
|
||||
## Aliased type aliases
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
MyIntOrStr = IntOrStr
|
||||
|
||||
x: MyIntOrStr = 1
|
||||
|
||||
# error: [invalid-assignment]
|
||||
y: MyIntOrStr = None
|
||||
```
|
||||
|
||||
## Generic type aliases
|
||||
|
||||
```py
|
||||
type ListOrSet[T] = list[T] | set[T]
|
||||
|
||||
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
|
||||
# as specified in the `typeshed` stubs.
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
@@ -1,117 +0,0 @@
|
||||
# type special form
|
||||
|
||||
## Class literal
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
```
|
||||
|
||||
## Nested class literal
|
||||
|
||||
```py
|
||||
class A:
|
||||
class B: ...
|
||||
|
||||
def f() -> type[A.B]:
|
||||
return A.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
```
|
||||
|
||||
## Deeply nested class literal
|
||||
|
||||
```py
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f() -> type[A.B.C]:
|
||||
return A.B.C
|
||||
|
||||
reveal_type(f()) # revealed: type[C]
|
||||
```
|
||||
|
||||
## Class literal from another module
|
||||
|
||||
```py
|
||||
from a import A
|
||||
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
class A: ...
|
||||
```
|
||||
|
||||
## Qualified class literal from another module
|
||||
|
||||
```py
|
||||
import a
|
||||
|
||||
def f() -> type[a.B]:
|
||||
return a.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
class B: ...
|
||||
```
|
||||
|
||||
## Deeply qualified class literal from another module
|
||||
|
||||
```py path=a/test.py
|
||||
import a.b
|
||||
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
def f() -> type[a.b.C]:
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
return a.b.C
|
||||
|
||||
reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Union of classes
|
||||
|
||||
```py
|
||||
class BasicUser: ...
|
||||
class ProUser: ...
|
||||
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def get_user() -> type[BasicUser | ProUser | A.B.C]:
|
||||
return BasicUser
|
||||
|
||||
# revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(get_user())
|
||||
```
|
||||
|
||||
## Illegal parameters
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def get_user() -> type[A, B]:
|
||||
return A
|
||||
```
|
||||
@@ -1,10 +1,6 @@
|
||||
# Invert, UAdd, USub
|
||||
|
||||
## Instance
|
||||
# Unary Operations
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Number:
|
||||
def __init__(self, value: int):
|
||||
self.value = 1
|
||||
@@ -22,7 +18,7 @@ a = Number()
|
||||
|
||||
reveal_type(+a) # revealed: int
|
||||
reveal_type(-a) # revealed: int
|
||||
reveal_type(~a) # revealed: Literal[True]
|
||||
reveal_type(~a) # revealed: @Todo
|
||||
|
||||
class NoDunder: ...
|
||||
|
||||
@@ -10,6 +10,8 @@ reveal_type(not not None) # revealed: Literal[False]
|
||||
## Function
|
||||
|
||||
```py
|
||||
from typing import reveal_type
|
||||
|
||||
def f():
|
||||
return 1
|
||||
|
||||
@@ -113,101 +115,3 @@ reveal_type(not ()) # revealed: Literal[True]
|
||||
reveal_type(not ("hello",)) # revealed: Literal[False]
|
||||
reveal_type(not (1, "hello")) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Instance
|
||||
|
||||
Not operator is inferred based on
|
||||
<https://docs.python.org/3/library/stdtypes.html#truth-value-testing>. An instance is True or False
|
||||
if the `__bool__` method says so.
|
||||
|
||||
At runtime, the `__len__` method is a fallback for `__bool__`, but we can't make use of that. If we
|
||||
have a class that defines `__len__` but not `__bool__`, it is possible that any subclass could add a
|
||||
`__bool__` method that would invalidate whatever conclusion we drew from `__len__`. So instances of
|
||||
classes without a `__bool__` method, with or without `__len__`, must be inferred as unknown
|
||||
truthiness.
|
||||
|
||||
```py
|
||||
class AlwaysTrue:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
# revealed: Literal[False]
|
||||
reveal_type(not AlwaysTrue())
|
||||
|
||||
class AlwaysFalse:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
# revealed: Literal[True]
|
||||
reveal_type(not AlwaysFalse())
|
||||
|
||||
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
class BoolIsBool:
|
||||
__bool__ = bool
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not BoolIsBool())
|
||||
|
||||
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
|
||||
# a subclass could add a `__bool__` method.
|
||||
class NoBoolMethod: ...
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not NoBoolMethod())
|
||||
|
||||
# And we can't rely on `__len__` for the same reason: a subclass could add `__bool__`.
|
||||
class LenZero:
|
||||
def __len__(self) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not LenZero())
|
||||
|
||||
class LenNonZero:
|
||||
def __len__(self) -> Literal[1]:
|
||||
return 1
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not LenNonZero())
|
||||
|
||||
class WithBothLenAndBool1:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
def __len__(self) -> Literal[2]:
|
||||
return 2
|
||||
|
||||
# revealed: Literal[True]
|
||||
reveal_type(not WithBothLenAndBool1())
|
||||
|
||||
class WithBothLenAndBool2:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def __len__(self) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
# revealed: Literal[False]
|
||||
reveal_type(not WithBothLenAndBool2())
|
||||
|
||||
# TODO: raise diagnostic when __bool__ method is not valid: [unsupported-operator] "Method __bool__ for type `MethodBoolInvalid` should return `bool`, returned type `int`"
|
||||
# https://docs.python.org/3/reference/datamodel.html#object.__bool__
|
||||
class MethodBoolInvalid:
|
||||
def __bool__(self) -> int:
|
||||
return 0
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not MethodBoolInvalid())
|
||||
|
||||
# Don't trust a possibly-unbound `__bool__` method:
|
||||
def get_flag() -> bool:
|
||||
return True
|
||||
|
||||
class PossiblyUnboundBool:
|
||||
if get_flag():
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not PossiblyUnboundBool())
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ reveal_type(b) # revealed: Literal[2]
|
||||
[a, *b, c, d] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
@@ -95,7 +95,7 @@ reveal_type(d) # revealed: Unknown
|
||||
[a, *b, c] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ reveal_type(c) # revealed: Literal[2]
|
||||
[a, *b, c] = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
@@ -115,7 +115,7 @@ reveal_type(c) # revealed: Literal[3]
|
||||
[a, *b, c, d] = (1, 2, 3, 4, 5, 6)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[5]
|
||||
reveal_type(d) # revealed: Literal[6]
|
||||
```
|
||||
@@ -127,7 +127,7 @@ reveal_type(d) # revealed: Literal[6]
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: @Todo
|
||||
```
|
||||
|
||||
### Starred expression (6)
|
||||
@@ -138,15 +138,20 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(d) # revealed: @Todo
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-iterable unpacking
|
||||
|
||||
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like assignment target,
|
||||
multiple definitions are created and the inference engine runs on each of them which results in
|
||||
duplicate diagnostics.
|
||||
|
||||
```py
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
a, b = 1
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -222,7 +227,7 @@ reveal_type(b) # revealed: LiteralString
|
||||
(a, *b, c, d) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
@@ -233,7 +238,7 @@ reveal_type(d) # revealed: Unknown
|
||||
(a, *b, c) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -243,7 +248,7 @@ reveal_type(c) # revealed: LiteralString
|
||||
(a, *b, c) = "abc"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -253,7 +258,7 @@ reveal_type(c) # revealed: LiteralString
|
||||
(a, *b, c, d) = "abcdef"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: LiteralString
|
||||
```
|
||||
@@ -265,44 +270,5 @@ reveal_type(d) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
### Unicode
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "é"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Unicode escape (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "\u9E6C"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Unicode escape (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
(a, b) = "\U0010FFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Surrogates
|
||||
|
||||
```py
|
||||
(a, b) = "\uD800\uDFFF"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
reveal_type(c) # revealed: @Todo
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::lint::RuleSelection;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
@@ -6,34 +5,26 @@ use ruff_db::{Db as SourceDb, Upcast};
|
||||
#[salsa::db]
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{default_lint_registry, ProgramSettings};
|
||||
|
||||
use super::Db;
|
||||
use crate::lint::RuleSelection;
|
||||
use anyhow::Context;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use super::Db;
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
@@ -42,9 +33,8 @@ pub(crate) mod tests {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
events: std::sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(&default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +97,6 @@ pub(crate) mod tests {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -122,66 +108,4 @@ pub(crate) mod tests {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestDbBuilder<'a> {
|
||||
/// Target Python version
|
||||
python_version: PythonVersion,
|
||||
/// Path to a custom typeshed directory
|
||||
custom_typeshed: Option<SystemPathBuf>,
|
||||
/// Path and content pairs for files that should be present
|
||||
files: Vec<(&'a str, &'a str)>,
|
||||
}
|
||||
|
||||
impl<'a> TestDbBuilder<'a> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
python_version: PythonVersion::default(),
|
||||
custom_typeshed: None,
|
||||
files: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_python_version(mut self, version: PythonVersion) -> Self {
|
||||
self.python_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
|
||||
self.custom_typeshed = Some(SystemPathBuf::from(path));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
|
||||
self.files.push((path, content));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system().create_directory_all(&src_root)?;
|
||||
|
||||
db.write_files(self.files)
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(src_root);
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: self.python_version,
|
||||
search_paths,
|
||||
},
|
||||
)
|
||||
.context("Failed to configure Program settings")?;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
TestDbBuilder::new().build().expect("valid TestDb setup")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::hash::BuildHasherDefault;
|
||||
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::lint::{LintRegistry, LintRegistryBuilder};
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, Module};
|
||||
@@ -12,7 +11,6 @@ pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod db;
|
||||
pub mod lint;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
@@ -24,17 +22,6 @@ pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub(crate) mod symbol;
|
||||
pub mod types;
|
||||
mod unpack;
|
||||
mod util;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
pub fn default_lint_registry() -> LintRegistry {
|
||||
let mut registry = LintRegistryBuilder::default();
|
||||
register_semantic_lints(&mut registry);
|
||||
registry.build()
|
||||
}
|
||||
|
||||
pub fn register_semantic_lints(registry: &mut LintRegistryBuilder) {
|
||||
types::register_type_lints(registry);
|
||||
}
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{LintName, Severity};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::hash::Hasher;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LintMetadata {
|
||||
/// The unique identifier for the lint.
|
||||
pub name: LintName,
|
||||
|
||||
/// A one-sentence summary of what the lint catches.
|
||||
pub summary: &'static str,
|
||||
|
||||
/// An in depth explanation of the lint in markdown. Covers what the lint does, why it's bad and possible fixes.
|
||||
///
|
||||
/// The documentation may require post-processing to be rendered correctly. For example, lines
|
||||
/// might have leading or trailing whitespace that should be removed.
|
||||
pub raw_documentation: &'static str,
|
||||
|
||||
/// The default level of the lint if the user doesn't specify one.
|
||||
pub default_level: Level,
|
||||
|
||||
pub status: LintStatus,
|
||||
|
||||
/// Location where this lint is declared: `file_name:line`
|
||||
pub source: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum Level {
|
||||
/// The lint is disabled and should not run.
|
||||
Ignore,
|
||||
|
||||
/// The lint is enabled and diagnostic should have a warning severity.
|
||||
Warn,
|
||||
|
||||
/// The lint is enabled and diagnostics have an error severity.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub const fn is_error(self) -> bool {
|
||||
matches!(self, Level::Error)
|
||||
}
|
||||
|
||||
pub const fn is_warn(self) -> bool {
|
||||
matches!(self, Level::Warn)
|
||||
}
|
||||
|
||||
pub const fn is_ignore(self) -> bool {
|
||||
matches!(self, Level::Ignore)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Level> for Severity {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(level: Level) -> Result<Self, ()> {
|
||||
match level {
|
||||
Level::Ignore => Err(()),
|
||||
Level::Warn => Ok(Severity::Warning),
|
||||
Level::Error => Ok(Severity::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LintMetadata {
|
||||
pub fn name(&self) -> LintName {
|
||||
self.name
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> &str {
|
||||
self.summary
|
||||
}
|
||||
|
||||
/// Returns the documentation line by line with leading and trailing whitespace removed.
|
||||
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
|
||||
self.raw_documentation
|
||||
.lines()
|
||||
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
|
||||
}
|
||||
|
||||
/// Returns the documentation as a single string.
|
||||
pub fn documentation(&self) -> String {
|
||||
self.documentation_lines().join("\n")
|
||||
}
|
||||
|
||||
pub fn default_level(&self) -> Level {
|
||||
self.default_level
|
||||
}
|
||||
|
||||
pub fn status(&self) -> &LintStatus {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &str {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub const fn lint_metadata_defaults() -> LintMetadata {
|
||||
LintMetadata {
|
||||
name: LintName::of(""),
|
||||
summary: "",
|
||||
raw_documentation: "",
|
||||
default_level: Level::Error,
|
||||
status: LintStatus::preview("0.0.0"),
|
||||
source: "",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum LintStatus {
|
||||
/// The rule has been added to the linter, but is not yet stable.
|
||||
Preview {
|
||||
/// When the rule was added to preview
|
||||
since: &'static str,
|
||||
},
|
||||
|
||||
/// Stable rule that was added in the version defined by `since`.
|
||||
Stable { since: &'static str },
|
||||
|
||||
/// The rule has been deprecated [`since`] (version) and will be removed in the future.
|
||||
Deprecated {
|
||||
since: &'static str,
|
||||
reason: &'static str,
|
||||
},
|
||||
|
||||
/// The rule has been removed [`since`] (version) and using it will result in an error.
|
||||
Removed {
|
||||
since: &'static str,
|
||||
reason: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl LintStatus {
|
||||
pub const fn preview(since: &'static str) -> Self {
|
||||
LintStatus::Preview { since }
|
||||
}
|
||||
|
||||
pub const fn stable(since: &'static str) -> Self {
|
||||
LintStatus::Stable { since }
|
||||
}
|
||||
|
||||
pub const fn deprecated(since: &'static str, reason: &'static str) -> Self {
|
||||
LintStatus::Deprecated { since, reason }
|
||||
}
|
||||
|
||||
pub const fn removed(since: &'static str, reason: &'static str) -> Self {
|
||||
LintStatus::Removed { since, reason }
|
||||
}
|
||||
|
||||
pub const fn is_removed(&self) -> bool {
|
||||
matches!(self, LintStatus::Removed { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! declare_lint {
|
||||
(
|
||||
$(#[doc = $doc:literal])+
|
||||
$vis: vis static $name: ident = {
|
||||
summary: $summary: literal,
|
||||
status: $status: expr,
|
||||
// Optional properties
|
||||
$( $key:ident: $value:expr, )*
|
||||
}
|
||||
) => {
|
||||
$( #[doc = $doc] )+
|
||||
#[allow(clippy::needless_update)]
|
||||
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
|
||||
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
|
||||
summary: $summary,
|
||||
raw_documentation: concat!($($doc,)+ "\n"),
|
||||
status: $status,
|
||||
source: concat!(file!(), ":", line!()),
|
||||
$( $key: $value, )*
|
||||
..$crate::lint::lint_metadata_defaults()
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// A unique identifier for a lint rule.
|
||||
///
|
||||
/// Implements `PartialEq`, `Eq`, and `Hash` based on the `LintMetadata` pointer
|
||||
/// for fast comparison and lookup.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LintId {
|
||||
definition: &'static LintMetadata,
|
||||
}
|
||||
|
||||
impl LintId {
|
||||
pub const fn of(definition: &'static LintMetadata) -> Self {
|
||||
LintId { definition }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LintId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(self.definition, other.definition)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for LintId {}
|
||||
|
||||
impl std::hash::Hash for LintId {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(self.definition, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LintId {
|
||||
type Target = LintMetadata;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.definition
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LintRegistryBuilder {
|
||||
/// Registered lints that haven't been removed.
|
||||
lints: Vec<LintId>,
|
||||
|
||||
/// Lints indexed by name, including aliases and removed rules.
|
||||
by_name: FxHashMap<&'static str, LintEntry>,
|
||||
}
|
||||
|
||||
impl LintRegistryBuilder {
|
||||
#[track_caller]
|
||||
pub fn register_lint(&mut self, lint: &'static LintMetadata) {
|
||||
assert_eq!(
|
||||
self.by_name.insert(&*lint.name, lint.into()),
|
||||
None,
|
||||
"duplicate lint registration for '{name}'",
|
||||
name = lint.name
|
||||
);
|
||||
|
||||
if !lint.status.is_removed() {
|
||||
self.lints.push(LintId::of(lint));
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn register_alias(&mut self, from: LintName, to: &'static LintMetadata) {
|
||||
let target = match self.by_name.get(to.name.as_str()) {
|
||||
Some(LintEntry::Lint(target) | LintEntry::Removed(target)) => target,
|
||||
Some(LintEntry::Alias(target)) => {
|
||||
panic!(
|
||||
"lint alias {from} -> {to:?} points to another alias {target:?}",
|
||||
target = target.name()
|
||||
)
|
||||
}
|
||||
None => panic!(
|
||||
"lint alias {from} -> {to} points to non-registered lint",
|
||||
to = to.name
|
||||
),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
self.by_name
|
||||
.insert(from.as_str(), LintEntry::Alias(*target)),
|
||||
None,
|
||||
"duplicate lint registration for '{from}'",
|
||||
);
|
||||
}
|
||||
|
||||
pub fn build(self) -> LintRegistry {
|
||||
LintRegistry {
|
||||
lints: self.lints,
|
||||
by_name: self.by_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LintRegistry {
|
||||
lints: Vec<LintId>,
|
||||
by_name: FxHashMap<&'static str, LintEntry>,
|
||||
}
|
||||
|
||||
impl LintRegistry {
|
||||
/// Looks up a lint by its name.
|
||||
pub fn get(&self, code: &str) -> Result<LintId, GetLintError> {
|
||||
match self.by_name.get(code) {
|
||||
Some(LintEntry::Lint(metadata)) => Ok(*metadata),
|
||||
Some(LintEntry::Alias(lint)) => {
|
||||
if lint.status.is_removed() {
|
||||
Err(GetLintError::Removed(lint.name()))
|
||||
} else {
|
||||
Ok(*lint)
|
||||
}
|
||||
}
|
||||
Some(LintEntry::Removed(lint)) => Err(GetLintError::Removed(lint.name())),
|
||||
None => Err(GetLintError::Unknown(code.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all registered, non-removed lints.
|
||||
pub fn lints(&self) -> &[LintId] {
|
||||
&self.lints
|
||||
}
|
||||
|
||||
/// Returns an iterator over all known aliases and to their target lints.
|
||||
///
|
||||
/// This iterator includes aliases that point to removed lints.
|
||||
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + use<'_> {
|
||||
self.by_name.iter().filter_map(|(key, value)| {
|
||||
if let LintEntry::Alias(alias) = value {
|
||||
Some((LintName::of(key), *alias))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterates over all removed lints.
|
||||
pub fn removed(&self) -> impl Iterator<Item = LintId> + use<'_> {
|
||||
self.by_name.iter().filter_map(|(_, value)| {
|
||||
if let LintEntry::Removed(metadata) = value {
|
||||
Some(*metadata)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum GetLintError {
|
||||
/// The name maps to this removed lint.
|
||||
#[error("lint {0} has been removed")]
|
||||
Removed(LintName),
|
||||
|
||||
/// No lint with the given name is known.
|
||||
#[error("unknown lint {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum LintEntry {
|
||||
/// An existing lint rule. Can be in preview, stable or deprecated.
|
||||
Lint(LintId),
|
||||
/// A lint rule that has been removed.
|
||||
Removed(LintId),
|
||||
Alias(LintId),
|
||||
}
|
||||
|
||||
impl From<&'static LintMetadata> for LintEntry {
|
||||
fn from(metadata: &'static LintMetadata) -> Self {
|
||||
if metadata.status.is_removed() {
|
||||
LintEntry::Removed(LintId::of(metadata))
|
||||
} else {
|
||||
LintEntry::Lint(LintId::of(metadata))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RuleSelection {
|
||||
/// Map with the severity for each enabled lint rule.
|
||||
///
|
||||
/// If a rule isn't present in this map, then it should be considered disabled.
|
||||
lints: FxHashMap<LintId, Severity>,
|
||||
}
|
||||
|
||||
impl RuleSelection {
|
||||
/// Creates a new rule selection from all known lints in the registry that are enabled
|
||||
/// according to their default severity.
|
||||
pub fn from_registry(registry: &LintRegistry) -> Self {
|
||||
let lints = registry
|
||||
.lints()
|
||||
.iter()
|
||||
.filter_map(|lint| {
|
||||
Severity::try_from(lint.default_level())
|
||||
.ok()
|
||||
.map(|severity| (*lint, severity))
|
||||
})
|
||||
.collect();
|
||||
|
||||
RuleSelection { lints }
|
||||
}
|
||||
|
||||
/// Returns an iterator over all enabled lints.
|
||||
pub fn enabled(&self) -> impl Iterator<Item = LintId> + use<'_> {
|
||||
self.lints.keys().copied()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all enabled lints and their severity.
|
||||
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + use<'_> {
|
||||
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
|
||||
}
|
||||
|
||||
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
|
||||
pub fn severity(&self, lint: LintId) -> Option<Severity> {
|
||||
self.lints.get(&lint).copied()
|
||||
}
|
||||
|
||||
/// Enables `lint` and configures with the given `severity`.
|
||||
///
|
||||
/// Overrides any previous configuration for the lint.
|
||||
pub fn enable(&mut self, lint: LintId, severity: Severity) {
|
||||
self.lints.insert(lint, severity);
|
||||
}
|
||||
|
||||
/// Disables `lint` if it was previously enabled.
|
||||
pub fn disable(&mut self, lint: LintId) {
|
||||
self.lints.remove(&lint);
|
||||
}
|
||||
|
||||
/// Merges the enabled lints from `other` into this selection.
|
||||
///
|
||||
/// Lints from `other` will override any existing configuration.
|
||||
pub fn merge(&mut self, other: &RuleSelection) {
|
||||
self.lints.extend(other.iter());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user