Compare commits
1 Commits
0.11.4
...
dcreager/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eff2734bb |
34
.github/workflows/build-binaries.yml
vendored
34
.github/workflows/build-binaries.yml
vendored
@@ -28,7 +28,7 @@ permissions: {}
|
||||
env:
|
||||
PACKAGE_NAME: ruff
|
||||
MODULE_NAME: ruff
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.11"
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -43,13 +43,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
@@ -72,14 +72,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
@@ -114,14 +114,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: arm64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
@@ -170,14 +170,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.platform.arch }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
@@ -223,14 +223,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
@@ -298,13 +298,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
@@ -363,14 +363,14 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -429,13 +429,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
|
||||
85
.github/workflows/ci.yaml
vendored
85
.github/workflows/ci.yaml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.12"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
@@ -36,8 +36,6 @@ jobs:
|
||||
code: ${{ steps.check_code.outputs.changed }}
|
||||
# Flag that is raised when any code that affects the fuzzer is changed
|
||||
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
|
||||
# Flag that is set to "true" when code related to red-knot changes.
|
||||
red_knot: ${{ steps.check_red_knot.outputs.changed }}
|
||||
|
||||
# Flag that is set to "true" when code related to the playground changes.
|
||||
playground: ${{ steps.check_playground.outputs.changed }}
|
||||
@@ -168,29 +166,6 @@ jobs:
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if the red-knot code changed
|
||||
id: check_red_knot
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':crates/red_knot*/**' \
|
||||
':crates/ruff_db/**' \
|
||||
':crates/ruff_annotate_snippets/**' \
|
||||
':crates/ruff_python_ast/**' \
|
||||
':crates/ruff_python_parser/**' \
|
||||
':crates/ruff_python_trivia/**' \
|
||||
':crates/ruff_source_file/**' \
|
||||
':crates/ruff_text_size/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
cargo-fmt:
|
||||
name: "cargo fmt"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -239,21 +214,13 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Red-knot mdtests (GitHub annotations)
|
||||
if: ${{ needs.determine_changes.outputs.red_knot == 'true' }}
|
||||
env:
|
||||
NO_COLOR: 1
|
||||
MDTEST_GITHUB_ANNOTATIONS_FORMAT: 1
|
||||
# Ignore errors if this step fails; we want to continue to later steps in the workflow anyway.
|
||||
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
|
||||
run: cargo test -p red_knot_python_semantic --test mdtest || true
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -293,11 +260,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -320,7 +287,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -403,11 +370,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -455,7 +422,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -521,7 +488,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -654,7 +621,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -662,7 +629,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
@@ -675,13 +642,20 @@ jobs:
|
||||
|
||||
pre-commit:
|
||||
name: "pre-commit"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install pre-commit"
|
||||
run: pip install pre-commit
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
with:
|
||||
@@ -692,7 +666,7 @@ jobs:
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -708,19 +682,19 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -779,10 +753,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
name: Download development ruff binary
|
||||
@@ -857,7 +830,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@914ac1e29db2d22aef69891f032778d9adc3990d # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -865,7 +838,7 @@ jobs:
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/daily_property_tests.yaml
vendored
2
.github/workflows/daily_property_tests.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
8
.github/workflows/mypy_primer.yaml
vendored
8
.github/workflows/mypy_primer.yaml
vendored
@@ -25,7 +25,7 @@ env:
|
||||
jobs:
|
||||
mypy_primer:
|
||||
name: Run mypy_primer
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Install mypy_primer
|
||||
run: |
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v3"
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
|
||||
|
||||
- name: Run mypy_primer
|
||||
shell: bash
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
--type-checker knot \
|
||||
--old base_commit \
|
||||
--new "$GITHUB_SHA" \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy)$' \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument)$' \
|
||||
--output concise \
|
||||
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
||||
|
||||
|
||||
2
.github/workflows/notify-dependents.yml
vendored
2
.github/workflows/notify-dependents.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Update pre-commit mirror"
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
|
||||
script: |
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
|
||||
|
||||
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@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@22695119d769bdb6f7032ad67b9bca0ef8c4a174 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist
|
||||
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2025 Astral Software Inc.
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
# 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/astral-sh/cargo-dist/releases/download/v0.28.3/cargo-dist-installer.sh | sh"
|
||||
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
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -19,7 +19,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.24.1
|
||||
rev: v0.24
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -60,7 +60,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.31.0
|
||||
rev: v1.30.2
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -74,7 +74,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.2
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -92,12 +92,12 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.5.2
|
||||
rev: v1.5.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.32.1
|
||||
rev: 0.31.3
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -1,53 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`ruff`\] Implement `invalid-rule-code` as `RUF102` ([#17138](https://github.com/astral-sh/ruff/pull/17138))
|
||||
- [syntax-errors] Detect duplicate keys in `match` mapping patterns ([#17129](https://github.com/astral-sh/ruff/pull/17129))
|
||||
- [syntax-errors] Detect duplicate attributes in `match` class patterns ([#17186](https://github.com/astral-sh/ruff/pull/17186))
|
||||
- [syntax-errors] Detect invalid syntax in annotations ([#17101](https://github.com/astral-sh/ruff/pull/17101))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [syntax-errors] Fix multiple assignment error for class fields in `match` patterns ([#17184](https://github.com/astral-sh/ruff/pull/17184))
|
||||
- Don't skip visiting non-tuple slice in `typing.Annotated` subscripts ([#17201](https://github.com/astral-sh/ruff/pull/17201))
|
||||
|
||||
## 0.11.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add more autofixes for `AIR302` ([#16876](https://github.com/astral-sh/ruff/pull/16876), [#16977](https://github.com/astral-sh/ruff/pull/16977), [#16976](https://github.com/astral-sh/ruff/pull/16976), [#16965](https://github.com/astral-sh/ruff/pull/16965))
|
||||
- \[`airflow`\] Move `AIR301` to `AIR002` ([#16978](https://github.com/astral-sh/ruff/pull/16978))
|
||||
- \[`airflow`\] Move `AIR302` to `AIR301` and `AIR303` to `AIR302` ([#17151](https://github.com/astral-sh/ruff/pull/17151))
|
||||
- \[`flake8-bandit`\] Mark `str` and `list[str]` literals as trusted input (`S603`) ([#17136](https://github.com/astral-sh/ruff/pull/17136))
|
||||
- \[`ruff`\] Support slices in `RUF005` ([#17078](https://github.com/astral-sh/ruff/pull/17078))
|
||||
- [syntax-errors] Start detecting compile-time syntax errors ([#16106](https://github.com/astral-sh/ruff/pull/16106))
|
||||
- [syntax-errors] Duplicate type parameter names ([#16858](https://github.com/astral-sh/ruff/pull/16858))
|
||||
- [syntax-errors] Irrefutable `case` pattern before final case ([#16905](https://github.com/astral-sh/ruff/pull/16905))
|
||||
- [syntax-errors] Multiple assignments in `case` pattern ([#16957](https://github.com/astral-sh/ruff/pull/16957))
|
||||
- [syntax-errors] Single starred assignment target ([#17024](https://github.com/astral-sh/ruff/pull/17024))
|
||||
- [syntax-errors] Starred expressions in `return`, `yield`, and `for` ([#17134](https://github.com/astral-sh/ruff/pull/17134))
|
||||
- [syntax-errors] Store to or delete `__debug__` ([#16984](https://github.com/astral-sh/ruff/pull/16984))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Error instead of `panic!` when running Ruff from a deleted directory (#16903) ([#17054](https://github.com/astral-sh/ruff/pull/17054))
|
||||
- [syntax-errors] Fix false positive for parenthesized tuple index ([#16948](https://github.com/astral-sh/ruff/pull/16948))
|
||||
|
||||
### CLI
|
||||
|
||||
- Check `pyproject.toml` correctly when it is passed via stdin ([#16971](https://github.com/astral-sh/ruff/pull/16971))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-import-conventions`\] Add import `numpy.typing as npt` to default `flake8-import-conventions.aliases` ([#17133](https://github.com/astral-sh/ruff/pull/17133))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`refurb`\] Document why `UserDict`, `UserList`, and `UserString` are preferred over `dict`, `list`, and `str` (`FURB189`) ([#16927](https://github.com/astral-sh/ruff/pull/16927))
|
||||
|
||||
## 0.11.2
|
||||
|
||||
### Preview features
|
||||
|
||||
69
Cargo.lock
generated
69
Cargo.lock
generated
@@ -207,9 +207,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.11"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6740c6e2fc6360fa57c35214c7493826aee95993926092606f27c983b40837be"
|
||||
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
@@ -334,9 +334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.34"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
||||
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -344,9 +344,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.34"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
||||
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1726,9 +1726,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -2503,23 +2503,6 @@ dependencies = [
|
||||
"wild",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_ide"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_project"
|
||||
version = "0.0.0"
|
||||
@@ -2531,7 +2514,6 @@ dependencies = [
|
||||
"notify",
|
||||
"pep440_rs",
|
||||
"rayon",
|
||||
"red_knot_ide",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"ruff_cache",
|
||||
@@ -2603,9 +2585,7 @@ dependencies = [
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"red_knot_ide",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -2666,7 +2646,6 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"log",
|
||||
"red_knot_ide",
|
||||
"red_knot_project",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
@@ -2755,7 +2734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.4"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2990,7 +2969,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.4"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3019,6 +2998,7 @@ dependencies = [
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
"ruff_diagnostics",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -3213,7 +3193,6 @@ name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
"ruff_index",
|
||||
@@ -3226,7 +3205,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"test-case",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3314,7 +3292,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.4"
|
||||
version = "0.11.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3408,7 +3386,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3421,7 +3399,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.3",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3439,7 +3417,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc#296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str 0.8.1",
|
||||
@@ -3455,19 +3433,18 @@ dependencies = [
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
"smallvec",
|
||||
"thin-vec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc#296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc#296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3807,7 +3784,7 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3880,12 +3857,6 @@ dependencies = [
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.84"
|
||||
rust-version = "1.83"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -38,11 +38,10 @@ ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
red_knot_vendored = { path = "crates/red_knot_vendored" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot_ide = { path = "crates/red_knot_ide" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
anstream = { version = "0.6.18" }
|
||||
@@ -124,7 +123,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "296a8c78da1b54c76ff5795eb4c1e3fe2467e9fc" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d758691ba17ee1a60c5356ea90888d529e1782ad" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -209,7 +208,6 @@ must_use_candidate = "allow"
|
||||
similar_names = "allow"
|
||||
single_match_else = "allow"
|
||||
too_many_lines = "allow"
|
||||
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
|
||||
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
|
||||
needless_raw_string_hashes = "allow"
|
||||
# Disallowed restriction lints
|
||||
@@ -272,7 +270,7 @@ inherits = "release"
|
||||
# Config for 'dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.28.3"
|
||||
cargo-dist-version = "0.25.2-prerelease.3"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
|
||||
@@ -149,8 +149,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.11.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.2/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,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.11.4
|
||||
rev: v0.11.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -15,7 +15,7 @@ use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{watch, Db};
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_server::run_server;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait, Severity};
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
@@ -288,7 +288,7 @@ impl MainLoop {
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
writeln!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
|
||||
failed |= diagnostic.severity() >= min_error_severity;
|
||||
}
|
||||
@@ -359,7 +359,7 @@ enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Diagnostic>,
|
||||
result: Vec<Box<dyn OldDiagnosticTrait>>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
|
||||
@@ -1125,11 +1125,11 @@ print(sys.last_exc, os.getegid())
|
||||
|
||||
assert_eq!(diagnostics.len(), 2);
|
||||
assert_eq!(
|
||||
diagnostics[0].primary_message(),
|
||||
diagnostics[0].message(),
|
||||
"Type `<module 'sys'>` has no attribute `last_exc`"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics[1].primary_message(),
|
||||
diagnostics[1].message(),
|
||||
"Type `<module 'os'>` has no attribute `getegid`"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "red_knot_ide"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,134 +0,0 @@
|
||||
use red_knot_python_semantic::Db as SemanticDb;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb + Upcast<dyn SemanticDb> + Upcast<dyn SourceDb> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::Db;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
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>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SemanticDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SemanticDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut dyn SemanticDb {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
/// Returns the node with a minimal range that fully contains `range`.
|
||||
///
|
||||
/// If `range` is empty and falls within a parser *synthesized* node generated during error recovery,
|
||||
/// then the first node with the given range is returned.
|
||||
///
|
||||
/// ## Panics
|
||||
/// Panics if `range` is not contained within `root`.
|
||||
pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
|
||||
struct Visitor<'a> {
|
||||
range: TextRange,
|
||||
found: bool,
|
||||
ancestors: Vec<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> SourceOrderVisitor<'a> for Visitor<'a> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||
// If the node fully contains the range, than it is a possible match but traverse into its children
|
||||
// to see if there's a node with a narrower range.
|
||||
if !self.found && node.range().contains_range(self.range) {
|
||||
self.ancestors.push(node);
|
||||
TraversalSignal::Traverse
|
||||
} else {
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
}
|
||||
|
||||
fn leave_node(&mut self, node: AnyNodeRef<'a>) {
|
||||
if !self.found && self.ancestors.last() == Some(&node) {
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
root.range().contains_range(range),
|
||||
"Range is not contained within root"
|
||||
);
|
||||
|
||||
let mut visitor = Visitor {
|
||||
range,
|
||||
found: false,
|
||||
ancestors: Vec::new(),
|
||||
};
|
||||
|
||||
root.visit_source_order(&mut visitor);
|
||||
|
||||
let minimal = visitor.ancestors.pop().unwrap_or(root);
|
||||
CoveringNode {
|
||||
node: minimal,
|
||||
ancestors: visitor.ancestors,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
pub(crate) struct CoveringNode<'a> {
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
node: AnyNodeRef<'a>,
|
||||
|
||||
/// The node's ancestor (the spine up to the root).
|
||||
ancestors: Vec<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> CoveringNode<'a> {
|
||||
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
|
||||
self.node
|
||||
}
|
||||
|
||||
/// Returns the node's parent.
|
||||
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
|
||||
self.ancestors.last().copied()
|
||||
}
|
||||
|
||||
/// Finds the minimal node that fully covers the range and fulfills the given predicate.
|
||||
pub(crate) fn find(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
|
||||
if f(self.node) {
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
match self.ancestors.iter().rposition(|node| f(*node)) {
|
||||
Some(index) => {
|
||||
let node = self.ancestors[index];
|
||||
self.ancestors.truncate(index);
|
||||
|
||||
Ok(Self {
|
||||
node,
|
||||
ancestors: self.ancestors,
|
||||
})
|
||||
}
|
||||
None => Err(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CoveringNode<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("NodeWithAncestors")
|
||||
.field(&self.node)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -1,851 +0,0 @@
|
||||
use crate::find_node::covering_node;
|
||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::{HasType, SemanticModel};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
pub fn goto_type_definition(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
offset: TextSize,
|
||||
) -> Option<RangedValue<NavigationTargets>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
let ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!(
|
||||
"Inferred type of covering node is {}",
|
||||
ty.display(db.upcast())
|
||||
);
|
||||
|
||||
let navigation_targets = ty.navigation_targets(db);
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: navigation_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum GotoTarget<'a> {
|
||||
Expression(ast::ExprRef<'a>),
|
||||
FunctionDef(&'a ast::StmtFunctionDef),
|
||||
ClassDef(&'a ast::StmtClassDef),
|
||||
Parameter(&'a ast::Parameter),
|
||||
Alias(&'a ast::Alias),
|
||||
|
||||
/// Go to on the module name of an import from
|
||||
/// ```py
|
||||
/// from foo import bar
|
||||
/// ^^^
|
||||
/// ```
|
||||
ImportedModule(&'a ast::StmtImportFrom),
|
||||
|
||||
/// Go to on the exception handler variable
|
||||
/// ```py
|
||||
/// try: ...
|
||||
/// except Exception as e: ...
|
||||
/// ^
|
||||
/// ```
|
||||
ExceptVariable(&'a ast::ExceptHandlerExceptHandler),
|
||||
|
||||
/// Go to on a keyword argument
|
||||
/// ```py
|
||||
/// test(a = 1)
|
||||
/// ^
|
||||
/// ```
|
||||
KeywordArgument(&'a ast::Keyword),
|
||||
|
||||
/// Go to on the rest parameter of a pattern match
|
||||
///
|
||||
/// ```py
|
||||
/// match x:
|
||||
/// case {"a": a, "b": b, **rest}: ...
|
||||
/// ^^^^
|
||||
/// ```
|
||||
PatternMatchRest(&'a ast::PatternMatchMapping),
|
||||
|
||||
/// Go to on a keyword argument of a class pattern
|
||||
///
|
||||
/// ```py
|
||||
/// match Point3D(0, 0, 0):
|
||||
/// case Point3D(x=0, y=0, z=0): ...
|
||||
/// ^ ^ ^
|
||||
/// ```
|
||||
PatternKeywordArgument(&'a ast::PatternKeyword),
|
||||
|
||||
/// Go to on a pattern star argument
|
||||
///
|
||||
/// ```py
|
||||
/// match array:
|
||||
/// case [*args]: ...
|
||||
/// ^^^^
|
||||
PatternMatchStarName(&'a ast::PatternMatchStar),
|
||||
|
||||
/// Go to on the name of a pattern match as pattern
|
||||
///
|
||||
/// ```py
|
||||
/// match x:
|
||||
/// case [x] as y: ...
|
||||
/// ^
|
||||
PatternMatchAsName(&'a ast::PatternMatchAs),
|
||||
|
||||
/// Go to on the name of a type variable
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[T: int = bool] = list[T]
|
||||
/// ^
|
||||
/// ```
|
||||
TypeParamTypeVarName(&'a ast::TypeParamTypeVar),
|
||||
|
||||
/// Go to on the name of a type param spec
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[**P = [int, str]] = Callable[P, int]
|
||||
/// ^
|
||||
/// ```
|
||||
TypeParamParamSpecName(&'a ast::TypeParamParamSpec),
|
||||
|
||||
/// Go to on the name of a type var tuple
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[*Ts = ()] = tuple[*Ts]
|
||||
/// ^^
|
||||
/// ```
|
||||
TypeParamTypeVarTupleName(&'a ast::TypeParamTypeVarTuple),
|
||||
|
||||
NonLocal {
|
||||
identifier: &'a ast::Identifier,
|
||||
},
|
||||
Globals {
|
||||
identifier: &'a ast::Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> GotoTarget<'db> {
|
||||
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||
let ty = match self {
|
||||
GotoTarget::Expression(expression) => expression.inferred_type(model),
|
||||
GotoTarget::FunctionDef(function) => function.inferred_type(model),
|
||||
GotoTarget::ClassDef(class) => class.inferred_type(model),
|
||||
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
|
||||
GotoTarget::Alias(alias) => alias.inferred_type(model),
|
||||
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
||||
GotoTarget::KeywordArgument(argument) => {
|
||||
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
|
||||
// than using the inferred value.
|
||||
argument.value.inferred_type(model)
|
||||
}
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::ImportedModule(_)
|
||||
| GotoTarget::TypeParamTypeVarName(_)
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
| GotoTarget::NonLocal { .. }
|
||||
| GotoTarget::Globals { .. } => return None,
|
||||
};
|
||||
|
||||
Some(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => expression.range(),
|
||||
GotoTarget::FunctionDef(function) => function.name.range,
|
||||
GotoTarget::ClassDef(class) => class.name.range,
|
||||
GotoTarget::Parameter(parameter) => parameter.name.range,
|
||||
GotoTarget::Alias(alias) => alias.name.range,
|
||||
GotoTarget::ImportedModule(module) => module.module.as_ref().unwrap().range,
|
||||
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
|
||||
GotoTarget::KeywordArgument(keyword) => keyword.arg.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
|
||||
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
|
||||
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchAsName(as_name) => as_name.name.as_ref().unwrap().range,
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => type_var.name.range,
|
||||
GotoTarget::TypeParamParamSpecName(spec) => spec.name.range,
|
||||
GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range,
|
||||
GotoTarget::NonLocal { identifier, .. } => identifier.range,
|
||||
GotoTarget::Globals { identifier, .. } => identifier.range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
||||
let token = parsed
|
||||
.tokens()
|
||||
.at_offset(offset)
|
||||
.max_by_key(|token| match token.kind() {
|
||||
TokenKind::Name
|
||||
| TokenKind::String
|
||||
| TokenKind::Complex
|
||||
| TokenKind::Float
|
||||
| TokenKind::Int => 1,
|
||||
_ => 0,
|
||||
})?;
|
||||
|
||||
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
||||
.find(|node| node.is_identifier() || node.is_expression())
|
||||
.ok()?;
|
||||
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
|
||||
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
||||
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
||||
Some(AnyNodeRef::Alias(alias)) => Some(GotoTarget::Alias(alias)),
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => Some(GotoTarget::ImportedModule(from)),
|
||||
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => Some(GotoTarget::TypeParamTypeVarName(var)),
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{cursor_test, CursorTest, IntoDiagnostic};
|
||||
use crate::{goto_type_definition, NavigationTarget};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_class_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Test: ...
|
||||
|
||||
a<CURSOR>b = Test()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:19
|
||||
|
|
||||
2 | class Test: ...
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | class Test: ...
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
| ^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_function_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
ab = foo
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | ab = foo
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:6:13
|
||||
|
|
||||
4 | ab = foo
|
||||
5 |
|
||||
6 | ab
|
||||
| ^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_union_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice():
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:3:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
| ^^^
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:5:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
| ^^^
|
||||
6 |
|
||||
7 | if random.choice():
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
lib<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "test"
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | a: str = "test"
|
||||
3 |
|
||||
4 | a
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "te<CURSOR>st"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:2:22
|
||||
|
|
||||
2 | a: str = "test"
|
||||
| ^^^^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:24
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_param_spec() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type param specs
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_tuple() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type var tuples
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= "123")
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= "123")
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should jump to `str` and not `int` because
|
||||
// the keyword is typed as a string. It's only the passed argument that
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:234:7
|
||||
|
|
||||
232 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
233 |
|
||||
234 | class int:
|
||||
| ^^^
|
||||
235 | @overload
|
||||
236 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_kwargs() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def f(name: str): ...
|
||||
|
||||
kwargs = { "name": "test"}
|
||||
|
||||
f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:1098:7
|
||||
|
|
||||
1096 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
1097 |
|
||||
1098 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
1099 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics
|
||||
1100 | # Also multiprocessing.managers.SyncManager.dict()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:6:5
|
||||
|
|
||||
4 | kwargs = { "name": "test"}
|
||||
5 |
|
||||
6 | f(**kwargs)
|
||||
| ^^^^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class X:
|
||||
def foo(a, b): ...
|
||||
|
||||
x = X()
|
||||
|
||||
x<CURSOR>.foo()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:19
|
||||
|
|
||||
2 | class X:
|
||||
| ^
|
||||
3 | def foo(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:7:13
|
||||
|
|
||||
5 | x = X()
|
||||
6 |
|
||||
7 | x.foo()
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_between_call_arguments() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | foo()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo()
|
||||
| ^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_narrowing() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_none() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/types.pyi:677:11
|
||||
|
|
||||
675 | if sys.version_info >= (3, 10):
|
||||
676 | @final
|
||||
677 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
678 | def __bool__(self) -> Literal[False]: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
|
|
||||
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
442 |
|
||||
443 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
444 | @overload
|
||||
445 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No type definitions found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTypeDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoTypeDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
let mut main = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("goto-type-definition")),
|
||||
Severity::Info,
|
||||
"Type definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
use crate::goto::find_goto_target;
|
||||
use crate::{Db, MarkupKind, RangedValue};
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::SemanticModel;
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
let ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!(
|
||||
"Inferred type of covering node is {}",
|
||||
ty.display(db.upcast())
|
||||
);
|
||||
|
||||
// TODO: Add documentation of the symbol (not the type's definition).
|
||||
// TODO: Render the symbol's signature instead of just its type.
|
||||
let contents = vec![HoverContent::Type(ty)];
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: Hover { contents },
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Hover<'db> {
|
||||
contents: Vec<HoverContent<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> Hover<'db> {
|
||||
/// Renders the hover to a string using the specified markup kind.
|
||||
pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> {
|
||||
DisplayHover {
|
||||
db,
|
||||
hover: self,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> std::slice::Iter<'_, HoverContent<'db>> {
|
||||
self.contents.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> IntoIterator for Hover<'db> {
|
||||
type Item = HoverContent<'db>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.contents.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a Hover<'db> {
|
||||
type Item = &'a HoverContent<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, HoverContent<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayHover<'a> {
|
||||
db: &'a dyn Db,
|
||||
hover: &'a Hover<'a>,
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayHover<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut first = true;
|
||||
for content in &self.hover.contents {
|
||||
if !first {
|
||||
self.kind.horizontal_line().fmt(f)?;
|
||||
}
|
||||
|
||||
content.display(self.db, self.kind).fmt(f)?;
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum HoverContent<'db> {
|
||||
Type(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> HoverContent<'db> {
|
||||
fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> {
|
||||
DisplayHoverContent {
|
||||
db,
|
||||
content: self,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayHoverContent<'a, 'db> {
|
||||
db: &'db dyn Db,
|
||||
content: &'a HoverContent<'db>,
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayHoverContent<'_, '_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.content {
|
||||
HoverContent::Type(ty) => self
|
||||
.kind
|
||||
.fenced_code_block(ty.display(self.db.upcast()), "text")
|
||||
.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{cursor_test, CursorTest};
|
||||
use crate::{hover, MarkupKind};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
||||
Severity, Span,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
#[test]
|
||||
fn hover_basic() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a = 10
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Literal[10]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[10]
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:9
|
||||
|
|
||||
2 | a = 10
|
||||
3 |
|
||||
4 | a
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_member() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Foo:
|
||||
a: int = 10
|
||||
|
||||
def __init__(a: int, b: str):
|
||||
self.a = a
|
||||
self.b: str = b
|
||||
|
||||
foo = Foo()
|
||||
foo.<CURSOR>a
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
int
|
||||
---------------------------------------------
|
||||
```text
|
||||
int
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:10:9
|
||||
|
|
||||
9 | foo = Foo()
|
||||
10 | foo.a
|
||||
| ^^^^-
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_function_typed_variable() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Literal[foo]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[foo]
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo
|
||||
| ^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_binary_expression() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: int, b: int, c: int):
|
||||
a + b ==<CURSOR> c
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
bool
|
||||
---------------------------------------------
|
||||
```text
|
||||
bool
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: int, b: int, c: int):
|
||||
3 | a + b == c
|
||||
| ^^^^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_keyword_parameter() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: int): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should reveal `int` because the user hovers over the parameter and not the value.
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Literal[123]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[123]
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: int): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_union() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice([True, False]):
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Literal[foo, bar]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[foo, bar]
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
li<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'lib'>
|
||||
---------------------------------------------
|
||||
```text
|
||||
<module 'lib'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^-
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_of_expression_with_type_var_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
T
|
||||
---------------------------------------------
|
||||
```text
|
||||
T
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_of_expression_with_type_param_spec() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```text
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:2:53
|
||||
|
|
||||
2 | type Alias[**P = [int, str]] = Callable[P, int]
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_of_expression_with_type_var_tuple() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```text
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:2:43
|
||||
|
|
||||
2 | type Alias[*Ts = ()] = tuple[*Ts]
|
||||
| ^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_class_member_declaration() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Foo:
|
||||
a<CURSOR>: int
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Never
|
||||
---------------------------------------------
|
||||
```text
|
||||
Never
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:3:13
|
||||
|
|
||||
2 | class Foo:
|
||||
3 | a: int
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_narrowing() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
str
|
||||
---------------------------------------------
|
||||
```text
|
||||
str
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn hover(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let Some(hover) = hover(&self.db, self.file, self.cursor_offset) else {
|
||||
return "Hover provided no content".to_string();
|
||||
};
|
||||
|
||||
let source = hover.range;
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
write!(
|
||||
&mut buf,
|
||||
"{plaintext}{line}{markdown}{line}",
|
||||
plaintext = hover.display(&self.db, MarkupKind::PlainText),
|
||||
line = MarkupKind::PlainText.horizontal_line(),
|
||||
markdown = hover.display(&self.db, MarkupKind::Markdown),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("hover")),
|
||||
Severity::Info,
|
||||
"Hovered content is",
|
||||
);
|
||||
diagnostic.annotate(
|
||||
Annotation::primary(Span::from(source.file()).with_range(source.range()))
|
||||
.message("source"),
|
||||
);
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(
|
||||
Span::from(source.file()).with_range(TextRange::empty(self.cursor_offset)),
|
||||
)
|
||||
.message("Cursor offset"),
|
||||
);
|
||||
|
||||
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
mod db;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
mod hover;
|
||||
mod markup;
|
||||
|
||||
pub use db::Db;
|
||||
pub use goto::goto_type_definition;
|
||||
pub use hover::hover;
|
||||
pub use markup::MarkupKind;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use red_knot_python_semantic::types::{Type, TypeDefinition};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
/// Information associated with a text range.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RangedValue<T> {
|
||||
pub range: FileRange,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
impl<T> RangedValue<T> {
|
||||
pub fn file_range(&self) -> FileRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for RangedValue<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for RangedValue<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for RangedValue<T>
|
||||
where
|
||||
T: IntoIterator,
|
||||
{
|
||||
type Item = T::Item;
|
||||
type IntoIter = T::IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.value.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Target to which the editor can navigate to.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NavigationTarget {
|
||||
file: File,
|
||||
|
||||
/// The range that should be focused when navigating to the target.
|
||||
///
|
||||
/// This is typically not the full range of the node. For example, it's the range of the class's name in a class definition.
|
||||
///
|
||||
/// The `focus_range` must be fully covered by `full_range`.
|
||||
focus_range: TextRange,
|
||||
|
||||
/// The range covering the entire target.
|
||||
full_range: TextRange,
|
||||
}
|
||||
|
||||
impl NavigationTarget {
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
pub fn focus_range(&self) -> TextRange {
|
||||
self.focus_range
|
||||
}
|
||||
|
||||
pub fn full_range(&self) -> TextRange {
|
||||
self.full_range
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>);
|
||||
|
||||
impl NavigationTargets {
|
||||
fn single(target: NavigationTarget) -> Self {
|
||||
Self(smallvec::smallvec![target])
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self(smallvec::SmallVec::new())
|
||||
}
|
||||
|
||||
fn unique(targets: impl IntoIterator<Item = NavigationTarget>) -> Self {
|
||||
let unique: FxHashSet<_> = targets.into_iter().collect();
|
||||
if unique.is_empty() {
|
||||
Self::empty()
|
||||
} else {
|
||||
let mut targets = unique.into_iter().collect::<Vec<_>>();
|
||||
targets.sort_by_key(|target| (target.file, target.focus_range.start()));
|
||||
Self(targets.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> std::slice::Iter<'_, NavigationTarget> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for NavigationTargets {
|
||||
type Item = NavigationTarget;
|
||||
type IntoIter = smallvec::IntoIter<[NavigationTarget; 1]>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a NavigationTargets {
|
||||
type Item = &'a NavigationTarget;
|
||||
type IntoIter = std::slice::Iter<'a, NavigationTarget>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<NavigationTarget> for NavigationTargets {
|
||||
fn from_iter<T: IntoIterator<Item = NavigationTarget>>(iter: T) -> Self {
|
||||
Self::unique(iter)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasNavigationTargets {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets;
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for Type<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
Type::Union(union) => union
|
||||
.iter(db.upcast())
|
||||
.flat_map(|target| target.navigation_targets(db))
|
||||
.collect(),
|
||||
|
||||
Type::Intersection(intersection) => {
|
||||
// Only consider the positive elements because the negative elements are mainly from narrowing constraints.
|
||||
let mut targets = intersection
|
||||
.iter_positive(db.upcast())
|
||||
.filter(|ty| !ty.is_unknown());
|
||||
|
||||
let Some(first) = targets.next() else {
|
||||
return NavigationTargets::empty();
|
||||
};
|
||||
|
||||
match targets.next() {
|
||||
Some(_) => {
|
||||
// If there are multiple types in the intersection, we can't navigate to a single one
|
||||
// because the type is the intersection of all those types.
|
||||
NavigationTargets::empty()
|
||||
}
|
||||
None => first.navigation_targets(db),
|
||||
}
|
||||
}
|
||||
|
||||
ty => ty
|
||||
.definition(db.upcast())
|
||||
.map(|definition| definition.navigation_targets(db))
|
||||
.unwrap_or_else(NavigationTargets::empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for TypeDefinition<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let full_range = self.full_range(db.upcast());
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: full_range.file(),
|
||||
focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(),
|
||||
full_range: full_range.range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use red_knot_python_semantic::{
|
||||
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
pub(super) fn cursor_test(source: &str) -> CursorTest {
|
||||
let mut db = TestDb::new();
|
||||
let cursor_offset = source.find("<CURSOR>").expect(
|
||||
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
|
||||
);
|
||||
|
||||
let mut content = source[..cursor_offset].to_string();
|
||||
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
|
||||
|
||||
db.write_file("main.py", &content)
|
||||
.expect("write to memory file system to be successful");
|
||||
|
||||
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::latest(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![SystemPathBuf::from("/")],
|
||||
custom_typeshed: None,
|
||||
python_path: PythonPath::KnownSitePackages(vec![]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("Default settings to be valid");
|
||||
|
||||
let mut insta_settings = insta::Settings::clone_current();
|
||||
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
// Filter out TODO types because they are different between debug and release builds.
|
||||
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
|
||||
|
||||
let insta_settings_guard = insta_settings.bind_to_scope();
|
||||
|
||||
CursorTest {
|
||||
db,
|
||||
cursor_offset: TextSize::try_from(cursor_offset)
|
||||
.expect("source to be smaller than 4GB"),
|
||||
file,
|
||||
_insta_settings_guard: insta_settings_guard,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct CursorTest {
|
||||
pub(super) db: TestDb,
|
||||
pub(super) cursor_offset: TextSize,
|
||||
pub(super) file: File,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
pub(super) fn write_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> std::io::Result<()> {
|
||||
self.db.write_file(path, content)
|
||||
}
|
||||
|
||||
pub(super) fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = D>,
|
||||
D: IntoDiagnostic,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
for diagnostic in diagnostics {
|
||||
let diag = diagnostic.into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait IntoDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum MarkupKind {
|
||||
PlainText,
|
||||
Markdown,
|
||||
}
|
||||
|
||||
impl MarkupKind {
|
||||
pub(crate) const fn fenced_code_block<T>(self, code: T, language: &str) -> FencedCodeBlock<T>
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
FencedCodeBlock {
|
||||
language,
|
||||
code,
|
||||
kind: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn horizontal_line(self) -> HorizontalLine {
|
||||
HorizontalLine { kind: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FencedCodeBlock<'a, T> {
|
||||
language: &'a str,
|
||||
code: T,
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl<T> fmt::Display for FencedCodeBlock<'_, T>
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.kind {
|
||||
MarkupKind::PlainText => self.code.fmt(f),
|
||||
MarkupKind::Markdown => write!(
|
||||
f,
|
||||
"```{language}\n{code}\n```",
|
||||
language = self.language,
|
||||
code = self.code
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct HorizontalLine {
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl fmt::Display for HorizontalLine {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.kind {
|
||||
MarkupKind::PlainText => {
|
||||
f.write_str("\n---------------------------------------------\n")
|
||||
}
|
||||
MarkupKind::Markdown => {
|
||||
write!(f, "\n---\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ ruff_db = { workspace = true, features = ["cache", "serde"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["serde"] }
|
||||
ruff_text_size = { workspace = true }
|
||||
red_knot_ide = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true, features = ["serde"] }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ use ruff_python_ast::PythonVersion;
|
||||
/// resolved extra search path of `["b", "c", "a"]`, which means `a` will be tried last.
|
||||
///
|
||||
/// There's an argument here that the user should be able to specify the order of the paths,
|
||||
/// because only then is the user in full control of where to insert the path when specifying `extra-paths`
|
||||
/// because only then is the user in full control of where to insert the path when specyifing `extra-paths`
|
||||
/// in multiple sources.
|
||||
///
|
||||
/// ## Macro
|
||||
|
||||
@@ -3,10 +3,9 @@ use std::sync::Arc;
|
||||
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use red_knot_ide::Db as IdeDb;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
@@ -56,12 +55,12 @@ impl ProjectDatabase {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub fn check(&self) -> Result<Vec<Diagnostic>, Cancelled> {
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
self.with_db(|db| db.project().check(db))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Diagnostic>, Cancelled> {
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
self.with_db(|db| self.project().check_file(db, file))
|
||||
}
|
||||
|
||||
@@ -104,19 +103,6 @@ impl Upcast<dyn SourceDb> for ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn IdeDb> for ProjectDatabase {
|
||||
fn upcast(&self) -> &(dyn IdeDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut (dyn IdeDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl IdeDb for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for ProjectDatabase {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
@@ -159,7 +145,7 @@ impl salsa::Database for ProjectDatabase {
|
||||
}
|
||||
|
||||
let event = event();
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ impl ProjectDatabase {
|
||||
let program = Program::get(self);
|
||||
if let Err(error) = program.update_from_settings(self, program_settings) {
|
||||
tracing::error!("Failed to update the program settings, keeping the old program settings: {error}");
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.root() == project.root(self) {
|
||||
tracing::debug!("Reloading project after structural change");
|
||||
|
||||
@@ -9,9 +9,7 @@ pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
|
||||
use red_knot_python_semantic::register_lints;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::{
|
||||
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span,
|
||||
};
|
||||
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, OldParseDiagnostic, Severity, Span};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
@@ -19,6 +17,7 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use rustc_hash::FxHashSet;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -164,27 +163,24 @@ impl Project {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Diagnostic> {
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
diagnostics.extend(
|
||||
self.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic),
|
||||
);
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
let files = ProjectFiles::new(db, self);
|
||||
|
||||
diagnostics.extend(
|
||||
files
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.map(IOErrorDiagnostic::to_diagnostic),
|
||||
);
|
||||
diagnostics.extend(files.diagnostics().iter().cloned().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic);
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
let result = Arc::new(std::sync::Mutex::new(diagnostics));
|
||||
let inner_result = Arc::clone(&result);
|
||||
@@ -212,11 +208,14 @@ impl Project {
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut file_diagnostics: Vec<_> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic)
|
||||
.map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
})
|
||||
.collect();
|
||||
|
||||
let check_diagnostics = check_file_impl(db, file);
|
||||
@@ -399,36 +398,35 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(
|
||||
IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}
|
||||
.to_diagnostic(),
|
||||
);
|
||||
diagnostics.push(Box::new(IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
diagnostics.extend(
|
||||
parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| create_parse_diagnostic(file, error)),
|
||||
);
|
||||
diagnostics.extend(parsed.errors().iter().map(|error| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> =
|
||||
Box::new(OldParseDiagnostic::new(file, error.clone()));
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
diagnostic
|
||||
.primary_span()
|
||||
.span()
|
||||
.and_then(|span| span.range())
|
||||
.unwrap_or_default()
|
||||
.start()
|
||||
@@ -496,13 +494,21 @@ pub struct IOErrorDiagnostic {
|
||||
error: IOErrorKind,
|
||||
}
|
||||
|
||||
impl IOErrorDiagnostic {
|
||||
fn to_diagnostic(&self) -> Diagnostic {
|
||||
let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error);
|
||||
if let Some(file) = self.file {
|
||||
diag.annotate(Annotation::primary(Span::from(file)));
|
||||
}
|
||||
diag
|
||||
impl OldDiagnosticTrait for IOErrorDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.file.map(Span::from)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,6 +526,7 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file_impl, ProjectMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
||||
@@ -543,7 +550,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
);
|
||||
@@ -559,7 +566,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![] as Vec<String>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,14 @@ use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSou
|
||||
use crate::Db;
|
||||
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::diagnostic::{DiagnosticFormat, DiagnosticId, OldDiagnosticTrait, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -120,7 +121,7 @@ impl Options {
|
||||
.ok()
|
||||
.map(PythonPath::from_virtual_env_var)
|
||||
})
|
||||
.unwrap_or_else(|| PythonPath::Discover(project_root.to_path_buf())),
|
||||
.unwrap_or_else(|| PythonPath::KnownSitePackages(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,14 +397,22 @@ impl OptionDiagnostic {
|
||||
fn with_span(self, span: Option<Span>) -> Self {
|
||||
OptionDiagnostic { span, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_diagnostic(&self) -> Diagnostic {
|
||||
if let Some(ref span) = self.span {
|
||||
let mut diag = Diagnostic::new(self.id, self.severity, "");
|
||||
diag.annotate(Annotation::primary(span.clone()).message(&self.message));
|
||||
diag
|
||||
} else {
|
||||
Diagnostic::new(self.id, self.severity, &self.message)
|
||||
}
|
||||
impl OldDiagnosticTrait for OptionDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.message)
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.span.clone()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
self.severity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@ class MDTestRunner:
|
||||
|
||||
def watch(self) -> Never:
|
||||
self._recompile_tests("Compiling tests...", message_on_success=False)
|
||||
self._run_mdtest()
|
||||
self.console.print("[dim]Ready to watch for changes...[/dim]")
|
||||
|
||||
for changes in watch(CRATE_ROOT):
|
||||
|
||||
@@ -45,104 +45,3 @@ class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred self-reference annotations in a class definition
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Foo:
|
||||
this: Foo
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Baz = Foo
|
||||
```
|
||||
|
||||
## Non-deferred self-reference annotations in a class definition
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# error: [unresolved-reference]
|
||||
this: Foo
|
||||
ok: "Foo"
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
# error: [unresolved-reference]
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Qux = Foo
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# error: [unresolved-reference]
|
||||
def f(self) -> C:
|
||||
return self
|
||||
```
|
||||
|
||||
@@ -36,7 +36,7 @@ def f():
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(b1) # revealed: Unknown | Literal[0]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
|
||||
@@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
@@ -56,7 +56,7 @@ def _(
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
def foo(a_: e) -> None:
|
||||
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`)
|
||||
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec` instances in type expressions)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
@@ -52,11 +52,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, generics
|
||||
# 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)
|
||||
reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)]
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
|
||||
@@ -1541,7 +1541,7 @@ integers are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
|
||||
reveal_type((2).denominator) # revealed: Literal[1]
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1709,37 +1709,6 @@ reveal_type(C.a_type) # revealed: type
|
||||
reveal_type(C.a_none) # revealed: None
|
||||
```
|
||||
|
||||
## Enum classes
|
||||
|
||||
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
|
||||
|
||||
```py
|
||||
import enum
|
||||
|
||||
reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
|
||||
class Foo(enum.Enum):
|
||||
BAR = 1
|
||||
|
||||
reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
```
|
||||
|
||||
## `super()`
|
||||
|
||||
`super()` is not supported yet, but we do not emit false positives on `super()` calls.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def bar(self) -> int:
|
||||
return 42
|
||||
|
||||
class Bar(Foo):
|
||||
def bar(self) -> int:
|
||||
return super().bar()
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
|
||||
@@ -14,43 +14,43 @@ We support inference for all Python's binary operators: `+`, `-`, `*`, `@`, `/`,
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> "A":
|
||||
def __add__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __sub__(self, other) -> "A":
|
||||
def __sub__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __mul__(self, other) -> "A":
|
||||
def __mul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __matmul__(self, other) -> "A":
|
||||
def __matmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __truediv__(self, other) -> "A":
|
||||
def __truediv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __floordiv__(self, other) -> "A":
|
||||
def __floordiv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __mod__(self, other) -> "A":
|
||||
def __mod__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __pow__(self, other) -> "A":
|
||||
def __pow__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __lshift__(self, other) -> "A":
|
||||
def __lshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rshift__(self, other) -> "A":
|
||||
def __rshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __and__(self, other) -> "A":
|
||||
def __and__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __xor__(self, other) -> "A":
|
||||
def __xor__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __or__(self, other) -> "A":
|
||||
def __or__(self, other) -> A:
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
@@ -76,43 +76,43 @@ We also support inference for reflected operations:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __radd__(self, other) -> "A":
|
||||
def __radd__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rsub__(self, other) -> "A":
|
||||
def __rsub__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmul__(self, other) -> "A":
|
||||
def __rmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmatmul__(self, other) -> "A":
|
||||
def __rmatmul__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rtruediv__(self, other) -> "A":
|
||||
def __rtruediv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rfloordiv__(self, other) -> "A":
|
||||
def __rfloordiv__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rmod__(self, other) -> "A":
|
||||
def __rmod__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rpow__(self, other) -> "A":
|
||||
def __rpow__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rlshift__(self, other) -> "A":
|
||||
def __rlshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rrshift__(self, other) -> "A":
|
||||
def __rrshift__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rand__(self, other) -> "A":
|
||||
def __rand__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __rxor__(self, other) -> "A":
|
||||
def __rxor__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __ror__(self, other) -> "A":
|
||||
def __ror__(self, other) -> A:
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
@@ -157,11 +157,11 @@ the right-hand side is not a subtype of the left-hand side, `lhs.__add__` will t
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other: "B") -> int:
|
||||
def __add__(self, other: B) -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
def __radd__(self, other: "A") -> str:
|
||||
def __radd__(self, other: A) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
@@ -169,10 +169,10 @@ reveal_type(A() + B()) # revealed: int
|
||||
# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types,
|
||||
# the lhs *still* takes precedence
|
||||
class C:
|
||||
def __add__(self, other: "C") -> int:
|
||||
def __add__(self, other: C) -> int:
|
||||
return 42
|
||||
|
||||
def __radd__(self, other: "C") -> str:
|
||||
def __radd__(self, other: C) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(C() + C()) # revealed: int
|
||||
@@ -237,11 +237,11 @@ well.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __sub__(self, other: "A") -> "A":
|
||||
def __sub__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
class B:
|
||||
def __rsub__(self, other: A) -> "B":
|
||||
def __rsub__(self, other: A) -> B:
|
||||
return B()
|
||||
|
||||
reveal_type(A() - B()) # revealed: B
|
||||
@@ -300,10 +300,10 @@ its instance super-type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> "A":
|
||||
def __add__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __radd__(self, other) -> "A":
|
||||
def __radd__(self, other) -> A:
|
||||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
@@ -312,7 +312,7 @@ reveal_type(1 + A()) # revealed: A
|
||||
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 of overloaded function)
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -320,7 +320,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 of overloaded function)
|
||||
reveal_type(() + A()) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
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`:
|
||||
@@ -329,7 +329,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 of overloaded function)
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
@@ -371,39 +371,6 @@ a = NotBoolable()
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Operations on class objects
|
||||
|
||||
When operating on class objects, the corresponding dunder methods are looked up on the metaclass.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Meta(type):
|
||||
def __add__(self, other: Meta) -> int:
|
||||
return 1
|
||||
|
||||
def __lt__(self, other: Meta) -> bool:
|
||||
return True
|
||||
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return "a"
|
||||
|
||||
class A(metaclass=Meta): ...
|
||||
class B(metaclass=Meta): ...
|
||||
|
||||
reveal_type(A + B) # revealed: int
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A - B) # revealed: Unknown
|
||||
|
||||
reveal_type(A < B) # revealed: bool
|
||||
reveal_type(A > B) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A <= B) # revealed: Unknown
|
||||
|
||||
reveal_type(A[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
@@ -466,7 +433,7 @@ the unreflected dunder of the left-hand operand. For context, see
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __radd__(self, other: "Foo") -> "Foo":
|
||||
def __radd__(self, other: Foo) -> Foo:
|
||||
return self
|
||||
|
||||
# error: [unsupported-operator]
|
||||
|
||||
@@ -50,21 +50,9 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||
reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(2**x) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x**x) # revealed: @Todo(return type of overloaded function)
|
||||
```
|
||||
|
||||
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
||||
the second argument is >=0, an `int` is still returned:
|
||||
|
||||
```py
|
||||
reveal_type(1**0) # revealed: Literal[1]
|
||||
reveal_type(0**1) # revealed: Literal[0]
|
||||
reveal_type(0**0) # revealed: Literal[1]
|
||||
reveal_type((-1) ** 2) # revealed: Literal[1]
|
||||
reveal_type(2 ** (-1)) # revealed: float
|
||||
reveal_type((-1) ** (-1)) # revealed: float
|
||||
reveal_type(x**2) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(2**x) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(x**x) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
@@ -49,11 +49,3 @@ def f4(x: float, y: float):
|
||||
reveal_type(x // y) # revealed: int | float
|
||||
reveal_type(x % y) # revealed: int | float
|
||||
```
|
||||
|
||||
If any of the union elements leads to a division by zero, we will report an error:
|
||||
|
||||
```py
|
||||
def f5(m: int, n: Literal[-1, 0, 1]):
|
||||
# error: [division-by-zero] "Cannot divide object of type `int` by zero"
|
||||
return m / n
|
||||
```
|
||||
|
||||
@@ -26,11 +26,7 @@ reveal_type(type(1)) # revealed: Literal[int]
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
reveal_type(type("Foo", (), {})) # revealed: type
|
||||
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
|
||||
```
|
||||
|
||||
Other numbers of arguments are invalid
|
||||
@@ -42,60 +38,3 @@ type("Foo", ())
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# TODO: this should be an error
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# TODO: this should be an error
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
## Calls to `str()`
|
||||
|
||||
### Valid calls
|
||||
|
||||
```py
|
||||
str()
|
||||
str("")
|
||||
str(b"")
|
||||
str(1)
|
||||
str(object=1)
|
||||
|
||||
str(b"M\xc3\xbcsli", "utf-8")
|
||||
str(b"M\xc3\xbcsli", "utf-8", "replace")
|
||||
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16")
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore")
|
||||
|
||||
str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8")
|
||||
str(bytearray(), "utf-8")
|
||||
|
||||
str(encoding="utf-8", object=b"M\xc3\xbcsli")
|
||||
str(b"", errors="replace")
|
||||
str(encoding="utf-8")
|
||||
str(errors="replace")
|
||||
```
|
||||
|
||||
### Invalid calls
|
||||
|
||||
```py
|
||||
str(1, 2) # error: [no-matching-overload]
|
||||
str(o=1) # error: [no-matching-overload]
|
||||
|
||||
# First argument is not a bytes-like object:
|
||||
str("Müsli", "utf-8") # error: [no-matching-overload]
|
||||
|
||||
# Second argument is not a valid encoding:
|
||||
str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
@@ -204,28 +204,6 @@ def _(flag: bool):
|
||||
reveal_type(d[0]) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Calling a union of types without dunder methods
|
||||
|
||||
We add instance attributes here to make sure that we don't treat the implicit dunder calls here like
|
||||
regular method calls.
|
||||
|
||||
```py
|
||||
def external_getitem(instance, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class NotSubscriptable1:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
class NotSubscriptable2:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
def _(union: NotSubscriptable1 | NotSubscriptable2):
|
||||
# error: [non-subscriptable]
|
||||
union[0]
|
||||
```
|
||||
|
||||
## Calling a possibly-unbound dunder method
|
||||
|
||||
```py
|
||||
|
||||
@@ -43,7 +43,8 @@ def decorator(func) -> Callable[[], int]:
|
||||
def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
reveal_type(bar()) # revealed: int
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
@@ -59,7 +59,7 @@ import sys
|
||||
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[real]
|
||||
```
|
||||
|
||||
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
|
||||
|
||||
@@ -410,29 +410,23 @@ def does_nothing[T](f: T) -> T:
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
# TODO: no error should be emitted here (needs support for generics)
|
||||
# error: [invalid-argument-type]
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
# TODO: no error should be emitted here (needs support for generics)
|
||||
# error: [invalid-argument-type]
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: All of these should be `str` (and not emit an error), once we support generics
|
||||
# TODO: We do not support decorators yet (only limited special cases). Eventually,
|
||||
# these should all return `str`:
|
||||
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C.f1(1)) # revealed: Unknown
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C().f1(1)) # revealed: Unknown
|
||||
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C.f2(1)) # revealed: Unknown
|
||||
# error: [call-non-callable]
|
||||
reveal_type(C().f2(1)) # revealed: Unknown
|
||||
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||
|
||||
@@ -382,13 +382,13 @@ arbitrary objects to a `bool`, but a comparison of tuples will fail if the resul
|
||||
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
class A:
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
```
|
||||
|
||||
@@ -44,240 +44,6 @@ def _(target: int):
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Value match
|
||||
|
||||
A value pattern matches based on equality: the first `case` branch here will be taken if `subject`
|
||||
is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a
|
||||
custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider
|
||||
the possibility that the `case` branch might be taken even though the type `C` is disjoint from the
|
||||
type `Literal[2]`.
|
||||
|
||||
This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than
|
||||
`Literal[1]`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class C:
|
||||
pass
|
||||
|
||||
def _(subject: C):
|
||||
y = 1
|
||||
match subject:
|
||||
case 2:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Class match
|
||||
|
||||
A `case` branch with a class pattern is taken if the subject is an instance of the given class, and
|
||||
all subpatterns in the class pattern match.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
class FooSub(Foo):
|
||||
pass
|
||||
|
||||
class Bar:
|
||||
pass
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Bar():
|
||||
y = 3
|
||||
case Foo():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3, 4]
|
||||
|
||||
def _(target: FooSub | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3, 4]
|
||||
```
|
||||
|
||||
## Singleton match
|
||||
|
||||
Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(target: Literal[True, False]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: bool):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[4]
|
||||
|
||||
def _(target: None | Literal[True]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 4]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
|
||||
# bool is an int subclass
|
||||
def _(target: int):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Or match
|
||||
|
||||
A `|` pattern matches if any of the subpatterns match.
|
||||
|
||||
```py
|
||||
from typing import Literal, final
|
||||
|
||||
def _(target: Literal["foo", "baz"]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case "foo" | "bar":
|
||||
y = 2
|
||||
case "baz":
|
||||
y = 3
|
||||
|
||||
# TODO: with exhaustiveness, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case "foo" | 4 | True:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2]
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: int | None | float):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case Baz():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz() | True | False:
|
||||
y = 2
|
||||
case int():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
# Decorators
|
||||
|
||||
Decorators are a way to modify function and class behavior. A decorator is a callable that takes the
|
||||
function or class as an argument and returns a modified version of it.
|
||||
|
||||
## Basic example
|
||||
|
||||
A decorated function definition is conceptually similar to `def f(x): ...` followed by
|
||||
`f = decorator(f)`. This means that the type of a decorated function is the same as the return type
|
||||
of the decorator (which does not necessarily need to be a callable type):
|
||||
|
||||
```py
|
||||
def custom_decorator(f) -> int:
|
||||
return 1
|
||||
|
||||
@custom_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
## Type-annotated decorator
|
||||
|
||||
More commonly, a decorator returns a modified callable type:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_positive(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x > 0
|
||||
|
||||
@ensure_positive
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(4)) # revealed: bool
|
||||
```
|
||||
|
||||
## Decorators which take arguments
|
||||
|
||||
Decorators can be arbitrary expressions. This is often useful when the decorator itself takes
|
||||
arguments:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_larger_than(lower_bound: int) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]:
|
||||
def decorator(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x >= lower_bound
|
||||
return decorator
|
||||
|
||||
@ensure_larger_than(10)
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(14)) # revealed: bool
|
||||
```
|
||||
|
||||
## Multiple decorators
|
||||
|
||||
Multiple decorators can be applied to a single function. They are applied in "bottom-up" order,
|
||||
meaning that the decorator closest to the function definition is applied first:
|
||||
|
||||
```py
|
||||
def maps_to_str(f) -> str:
|
||||
return "a"
|
||||
|
||||
def maps_to_int(f) -> int:
|
||||
return 1
|
||||
|
||||
def maps_to_bytes(f) -> bytes:
|
||||
return b"a"
|
||||
|
||||
@maps_to_str
|
||||
@maps_to_int
|
||||
@maps_to_bytes
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Decorating with a class
|
||||
|
||||
When a function is decorated with a class-based decorator, the decorated function turns into an
|
||||
instance of the class (see also: [properties](properties.md)). Attributes of the class can be
|
||||
accessed on the decorated function.
|
||||
|
||||
```py
|
||||
class accept_strings:
|
||||
custom_attribute: str = "a"
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __call__(self, x: str | int) -> bool:
|
||||
return self.f(int(x))
|
||||
|
||||
@accept_strings
|
||||
def even(x: int) -> bool:
|
||||
return x > 0
|
||||
|
||||
reveal_type(even) # revealed: accept_strings
|
||||
reveal_type(even.custom_attribute) # revealed: str
|
||||
reveal_type(even("1")) # revealed: bool
|
||||
reveal_type(even(1)) # revealed: bool
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
even(None)
|
||||
```
|
||||
|
||||
## Common decorator patterns
|
||||
|
||||
### `functools.wraps`
|
||||
|
||||
This test mainly makes sure that we do not emit any diagnostics in a case where the decorator is
|
||||
implemented using `functools.wraps`.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from functools import wraps
|
||||
|
||||
def custom_decorator(f) -> Callable[[int], str]:
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
print("Calling decorated function")
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@custom_decorator
|
||||
def f(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
reveal_type(f) # revealed: (int, /) -> str
|
||||
```
|
||||
|
||||
### `functools.cache`
|
||||
|
||||
```py
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def f(x: int) -> int:
|
||||
return x**2
|
||||
|
||||
# TODO: Should be `_lru_cache_wrapper[int]`
|
||||
reveal_type(f) # revealed: @Todo(generics)
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(f(1)) # revealed: @Todo(generics)
|
||||
```
|
||||
|
||||
## Lambdas as decorators
|
||||
|
||||
```py
|
||||
@lambda f: f
|
||||
def g(x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: This should be `Literal[g]` or `(int, /) -> str`
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
### Unknown decorator
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `unknown_decorator` used when not defined"
|
||||
@unknown_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Error in the decorator expression
|
||||
|
||||
```py
|
||||
# error: [unsupported-operator]
|
||||
@(1 + "a")
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-callable decorator
|
||||
|
||||
```py
|
||||
non_callable = 1
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
@non_callable
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
#### Wrong argument type
|
||||
|
||||
Here, we emit a diagnostic since `wrong_signature` takes an `int` instead of a callable type as the
|
||||
first argument:
|
||||
|
||||
```py
|
||||
def wrong_signature(f: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 1 (`f`) of function `wrong_signature`; expected type `int`"
|
||||
@wrong_signature
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
#### Wrong number of arguments
|
||||
|
||||
Decorators need to be callable with a single argument. If they are not, we emit a diagnostic:
|
||||
|
||||
```py
|
||||
def takes_two_arguments(f, g) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `g` of function `takes_two_arguments`"
|
||||
@takes_two_arguments
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
|
||||
def takes_no_argument() -> str:
|
||||
return "a"
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `takes_no_argument`: expected 0, got 1"
|
||||
@takes_no_argument
|
||||
def g(x): ...
|
||||
```
|
||||
@@ -506,7 +506,8 @@ class C:
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name or "Unset"
|
||||
|
||||
# TODO: No diagnostic should be emitted here
|
||||
# error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`"
|
||||
@name.setter
|
||||
def name(self, value: str | None) -> None:
|
||||
self._value = value
|
||||
@@ -514,13 +515,22 @@ class C:
|
||||
c = C()
|
||||
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
reveal_type(c.name) # revealed: str
|
||||
reveal_type(C.name) # revealed: property
|
||||
|
||||
# TODO: Should be `str`
|
||||
reveal_type(c.name) # revealed: <bound method `name` of `C`>
|
||||
|
||||
# Should be `builtins.property`
|
||||
reveal_type(C.name) # revealed: Literal[name]
|
||||
|
||||
# TODO: These should not emit errors
|
||||
# error: [invalid-assignment]
|
||||
c.name = "new"
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.name = None
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C` with custom `__set__` method"
|
||||
# TODO: this should be an error, but with a proper error message
|
||||
# error: [invalid-assignment] "Implicit shadowing of function `name`; annotate to make it explicit if this is intentional"
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
@@ -577,7 +587,7 @@ reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
|
||||
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
|
||||
|
||||
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: str
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
We can also bind the free function `f` to an instance of a class `C`:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
The (inferred) type of the value and the given type do not need to have any correlation.
|
||||
|
||||
```py
|
||||
from typing import Literal, cast, Any
|
||||
from typing import Literal, cast
|
||||
|
||||
reveal_type(True) # revealed: Literal[True]
|
||||
reveal_type(cast(str, True)) # revealed: str
|
||||
@@ -25,46 +25,4 @@ reveal_type(cast(1, True)) # revealed: Unknown
|
||||
cast(str)
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
|
||||
cast(str, b"ar", "foo")
|
||||
|
||||
def function_returning_int() -> int:
|
||||
return 10
|
||||
|
||||
# error: [redundant-cast] "Value is already of type `int`"
|
||||
cast(int, function_returning_int())
|
||||
|
||||
def function_returning_any() -> Any:
|
||||
return "blah"
|
||||
|
||||
# error: [redundant-cast] "Value is already of type `Any`"
|
||||
cast(Any, function_returning_any())
|
||||
```
|
||||
|
||||
Complex type expressions (which may be unsupported) do not lead to spurious `[redundant-cast]`
|
||||
diagnostics.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
|
||||
a = cast(Callable[[list[bytes]], None], x)
|
||||
b = cast(tuple[list[bytes]], y)
|
||||
```
|
||||
|
||||
A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are
|
||||
understood as gradually equivalent types by red-knot, they are understood as different types by
|
||||
human readers of red-knot's output. For `Unknown` in particular, we may consider it differently in
|
||||
the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to
|
||||
an invalid annotation, missing annotation or missing type argument somewhere.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def f(x: Any, y: Unknown, z: Any | str | int):
|
||||
a = cast(dict[str, Any], x)
|
||||
reveal_type(a) # revealed: @Todo(generics)
|
||||
|
||||
b = cast(Any, y)
|
||||
reveal_type(b) # revealed: Any
|
||||
|
||||
c = cast(str | int | Any, z) # error: [redundant-cast]
|
||||
```
|
||||
|
||||
@@ -269,66 +269,3 @@ def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 2
|
||||
```
|
||||
|
||||
## NotImplemented
|
||||
|
||||
### Default Python version
|
||||
|
||||
`NotImplemented` is a special symbol in Python. It is commonly used to control the fallback behavior
|
||||
of special dunder methods. You can find more details in the
|
||||
[documentation](https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations).
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __add__(self, o: A) -> A:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
However, as shown below, `NotImplemented` should not cause issues with the declared return type.
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 1
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def f(x: int) -> int | str:
|
||||
if x < 0:
|
||||
return -1
|
||||
elif x == 0:
|
||||
return NotImplemented
|
||||
else:
|
||||
return "test"
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
# error: [invalid-return-type] "Object of type `Literal["hello"]` is not assignable to return type `int`"
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
||||
|
||||
### Python 3.10+
|
||||
|
||||
Unlike Ellipsis, `_NotImplementedType` remains in `builtins.pyi` regardless of the Python version.
|
||||
Even if `builtins._NotImplementedType` is fully replaced by `types.NotImplementedType` in the
|
||||
future, it should still work as expected.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return NotImplemented
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
||||
|
||||
@@ -107,7 +107,8 @@ def good_return[T: int](x: T) -> T:
|
||||
return x
|
||||
|
||||
def bad_return[T: int](x: T) -> T:
|
||||
# error: [invalid-return-type] "Object of type `int` is not assignable to return type `T`"
|
||||
# TODO: error: int is not assignable to T
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `Literal[1]`"
|
||||
return x + 1
|
||||
```
|
||||
|
||||
|
||||
@@ -48,523 +48,4 @@ class C[T]:
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
## Fully static typevars
|
||||
|
||||
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
|
||||
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
|
||||
is similar to how you can assign an expression whose type is not fully static to a target whose type
|
||||
is.)
|
||||
|
||||
```py
|
||||
from knot_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
def unbounded_unconstrained[T](t: list[T]) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded[T: int](t: list[T]) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
def constrained[T: (int, str)](t: list[T]) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
```
|
||||
|
||||
## Subtyping and assignability
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
|
||||
typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual
|
||||
typevars.)
|
||||
|
||||
We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be
|
||||
specialized to. Properties are true of the typevar only if they are true for every valid
|
||||
specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type
|
||||
(including other typevars).
|
||||
|
||||
```py
|
||||
from knot_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
|
||||
static_assert(is_assignable_to(T, T))
|
||||
static_assert(is_assignable_to(T, object))
|
||||
static_assert(not is_assignable_to(T, Super))
|
||||
static_assert(is_assignable_to(U, U))
|
||||
static_assert(is_assignable_to(U, object))
|
||||
static_assert(not is_assignable_to(U, Super))
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(is_subtype_of(T, T))
|
||||
static_assert(is_subtype_of(T, object))
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(is_subtype_of(U, U))
|
||||
static_assert(is_subtype_of(U, object))
|
||||
static_assert(not is_subtype_of(U, Super))
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
|
||||
its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does
|
||||
not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the
|
||||
typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound
|
||||
is a final class, since the typevar can still be specialized to `Never`.)
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import final
|
||||
|
||||
def bounded[T: Super](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(not is_assignable_to(Sub, T))
|
||||
|
||||
static_assert(is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Sub, T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, Any))
|
||||
static_assert(is_assignable_to(Any, T))
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(Sub, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Any))
|
||||
static_assert(not is_subtype_of(Any, T))
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(Sub, T))
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
def bounded_final[T: FinalClass](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, FinalClass))
|
||||
static_assert(not is_assignable_to(FinalClass, T))
|
||||
|
||||
static_assert(is_subtype_of(T, FinalClass))
|
||||
static_assert(not is_subtype_of(FinalClass, T))
|
||||
```
|
||||
|
||||
Two distinct fully static typevars are not subtypes of each other, even if they have the same
|
||||
bounds, since there is (still) no guarantee that they will be specialized to the same type. This is
|
||||
true even if both typevars are bounded by the same final class, since you can specialize the
|
||||
typevars to `Never` in addition to that final class.
|
||||
|
||||
```py
|
||||
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
|
||||
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A constrained fully static typevar is assignable to the union of its constraints, but not to any of
|
||||
the constraints individually. None of the constraints are subtypes of the typevar, though the
|
||||
intersection of all of its constraints is a subtype of the typevar.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection
|
||||
|
||||
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
|
||||
static_assert(not is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(T, Base))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(T, Unrelated))
|
||||
static_assert(is_assignable_to(T, Super | Unrelated))
|
||||
static_assert(is_assignable_to(T, Base | Unrelated))
|
||||
static_assert(not is_assignable_to(T, Sub | Unrelated))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(not is_assignable_to(Unrelated, T))
|
||||
static_assert(not is_assignable_to(Super | Unrelated, T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Base))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(T, Unrelated))
|
||||
static_assert(is_subtype_of(T, Super | Unrelated))
|
||||
static_assert(is_subtype_of(T, Base | Unrelated))
|
||||
static_assert(not is_subtype_of(T, Sub | Unrelated))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Unrelated, T))
|
||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
|
||||
|
||||
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(is_assignable_to(T, Base))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(T, Unrelated))
|
||||
static_assert(is_assignable_to(T, Any))
|
||||
static_assert(is_assignable_to(T, Super | Any))
|
||||
static_assert(is_assignable_to(T, Super | Unrelated))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(is_assignable_to(Base, T))
|
||||
static_assert(not is_assignable_to(Unrelated, T))
|
||||
static_assert(is_assignable_to(Any, T))
|
||||
static_assert(not is_assignable_to(Super | Any, T))
|
||||
static_assert(is_assignable_to(Base | Any, T))
|
||||
static_assert(not is_assignable_to(Super | Unrelated, T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Any], T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Base))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(T, Unrelated))
|
||||
static_assert(not is_subtype_of(T, Any))
|
||||
static_assert(not is_subtype_of(T, Super | Any))
|
||||
static_assert(not is_subtype_of(T, Super | Unrelated))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Base, T))
|
||||
static_assert(not is_subtype_of(Unrelated, T))
|
||||
static_assert(not is_subtype_of(Any, T))
|
||||
static_assert(not is_subtype_of(Super | Any, T))
|
||||
static_assert(not is_subtype_of(Base | Any, T))
|
||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
|
||||
static_assert(not is_subtype_of(Intersection[Base, Any], T))
|
||||
```
|
||||
|
||||
Two distinct fully static typevars are not subtypes of each other, even if they have the same
|
||||
constraints, and even if any of the constraints are final. There must always be at least two
|
||||
distinct constraints, meaning that there is (still) no guarantee that they will be specialized to
|
||||
the same type.
|
||||
|
||||
```py
|
||||
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
|
||||
@final
|
||||
class AnotherFinalClass: ...
|
||||
|
||||
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
## Singletons and single-valued types
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the
|
||||
claims also apply to _single-valued_ types.)
|
||||
|
||||
An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a
|
||||
non-singleton type.
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, is_single_valued, static_assert
|
||||
|
||||
def unbounded_unconstrained[T](t: list[T]) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
```
|
||||
|
||||
A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be
|
||||
specialized to `Never`.
|
||||
|
||||
```py
|
||||
def bounded[T: None](t: list[T]) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
```
|
||||
|
||||
A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot
|
||||
specialize a constrained typevar to a subtype of a constraint.)
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
|
||||
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
|
||||
static_assert(is_singleton(T))
|
||||
|
||||
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
|
||||
static_assert(is_single_valued(T))
|
||||
```
|
||||
|
||||
## Unions involving typevars
|
||||
|
||||
The union of an unbounded unconstrained typevar with any other type cannot be simplified, since
|
||||
there is no guarantee what type the typevar will be specialized to.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: T | Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: T | Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T | Sub
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be
|
||||
specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound
|
||||
cannot be simplified. (The typevar might be specialized to a different subtype of the bound.)
|
||||
|
||||
```py
|
||||
def bounded[T: Base](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T | Sub
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
The union of a constrained typevar with a type depends on how that type relates to the constraints.
|
||||
If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely,
|
||||
if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the
|
||||
union cannot be simplified.
|
||||
|
||||
```py
|
||||
def constrained[T: (Base, Sub)](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
## Intersections involving typevars
|
||||
|
||||
The intersection of an unbounded unconstrained typevar with any other type cannot be simplified,
|
||||
since there is no guarantee what type the typevar will be specialized to.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection
|
||||
from typing import Any
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
def _(x: Intersection[T, Super]) -> None:
|
||||
reveal_type(x) # revealed: T & Super
|
||||
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
reveal_type(x) # revealed: T & Base
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: T & Sub
|
||||
|
||||
def _(x: Intersection[T, Unrelated]) -> None:
|
||||
reveal_type(x) # revealed: T & Unrelated
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar
|
||||
itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded
|
||||
typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a
|
||||
different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint
|
||||
from its bound is `Never`.
|
||||
|
||||
```py
|
||||
def bounded[T: Base](t: T) -> None:
|
||||
def _(x: Intersection[T, Super]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: T & Sub
|
||||
|
||||
def _(x: Intersection[T, None]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must
|
||||
be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints,
|
||||
since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The
|
||||
`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it
|
||||
would not represent a single set of runtime objects; it would instead represent a _set of_ sets of
|
||||
runtime objects. This is one reason we have not actually added this connector to our data model yet.
|
||||
Nevertheless, describing constrained typevars this way helps explain how we simplify intersections
|
||||
involving them.
|
||||
|
||||
This means that when intersecting a constrained typevar with a type `T`, constraints that are
|
||||
supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover,
|
||||
constraints that are disjoint from `T` are no longer valid specializations of the typevar, since
|
||||
`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we
|
||||
can simplify the intersection as a whole to that constraint.
|
||||
|
||||
```py
|
||||
def constrained[T: (Base, Sub, Unrelated)](t: T) -> None:
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
# With OneOf this would be OneOf[Base, Sub]
|
||||
reveal_type(x) # revealed: T & Base
|
||||
|
||||
def _(x: Intersection[T, Unrelated]) -> None:
|
||||
reveal_type(x) # revealed: Unrelated
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: Sub
|
||||
|
||||
def _(x: Intersection[T, None]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
We can simplify the intersection similarly when removing a type from a constrained typevar, since
|
||||
this is modeled internally as an intersection with a negation.
|
||||
|
||||
```py
|
||||
from knot_extensions import Not
|
||||
|
||||
def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
def _(x: Intersection[T, Not[int]]) -> None:
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
|
||||
def _(x: Intersection[T, Not[str]]) -> None:
|
||||
# With OneOf this would be OneOf[int, bool]
|
||||
reveal_type(x) # revealed: T & ~str
|
||||
|
||||
def _(x: Intersection[T, Not[bool]]) -> None:
|
||||
reveal_type(x) # revealed: T & ~bool
|
||||
|
||||
def _(x: Intersection[T, Not[int], Not[str]]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Not[None]]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Not[Any]]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
|
||||
|
||||
```py
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def f[T: (P, Q)](t: T) -> None:
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
else:
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
|
||||
if isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q
|
||||
q: Q = t
|
||||
else:
|
||||
reveal_type(t) # revealed: P & ~Q
|
||||
p: P = t
|
||||
|
||||
def g[T: (P, Q, R)](t: T) -> None:
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
elif isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
else:
|
||||
reveal_type(t) # revealed: R & ~P & ~Q
|
||||
r: R = t
|
||||
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
elif isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
elif isinstance(t, R):
|
||||
reveal_type(t) # revealed: R & ~P & ~Q
|
||||
r: R = t
|
||||
else:
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
If the constraints are disjoint, simplification does eliminate the redundant negative:
|
||||
|
||||
```py
|
||||
def h[T: (P, None)](t: T) -> None:
|
||||
if t is None:
|
||||
reveal_type(t) # revealed: None
|
||||
p: None = t
|
||||
else:
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
```
|
||||
|
||||
[pep 695]: https://peps.python.org/pep-0695/
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Narrowing for `in` conditionals
|
||||
|
||||
## `in` for tuples
|
||||
|
||||
```py
|
||||
def _(x: int):
|
||||
if x in (1, 2, 3):
|
||||
reveal_type(x) # revealed: int
|
||||
else:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: str):
|
||||
if x in ("a", "b", "c"):
|
||||
reveal_type(x) # revealed: str
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal[1, 2, "a", "b", False, b"abc"]):
|
||||
if x in (1,):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
elif x in (2, "a"):
|
||||
reveal_type(x) # revealed: Literal[2, "a"]
|
||||
elif x in (b"abc",):
|
||||
reveal_type(x) # revealed: Literal[b"abc"]
|
||||
elif x not in (3,):
|
||||
reveal_type(x) # revealed: Literal["b", False]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: Literal["a", "b", "c", 1]):
|
||||
if x in ("a", "b", "c", 2):
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## `in` for `str` and literal strings
|
||||
|
||||
```py
|
||||
def _(x: str):
|
||||
if x in "abc":
|
||||
reveal_type(x) # revealed: str
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal["a", "b", "c", "d"]):
|
||||
if x in "abc":
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["d"]
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: Literal["a", "b", "c", "e"]):
|
||||
if x in "abcd":
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["e"]
|
||||
```
|
||||
|
||||
```py
|
||||
def _(x: Literal[1, "a", "b", "c", "d"]):
|
||||
# error: [unsupported-operator]
|
||||
if x in "abc":
|
||||
reveal_type(x) # revealed: Literal["a", "b", "c"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, "d"]
|
||||
```
|
||||
@@ -64,99 +64,3 @@ match x:
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Value patterns
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo":
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
case 42:
|
||||
reveal_type(x) # revealed: Literal[42]
|
||||
case 6.0:
|
||||
reveal_type(x) # revealed: float
|
||||
case 1j:
|
||||
reveal_type(x) # revealed: complex
|
||||
case b"foo":
|
||||
reveal_type(x) # revealed: Literal[b"foo"]
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Value patterns with guard
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo" if reveal_type(x): # revealed: Literal["foo"]
|
||||
pass
|
||||
case 42 if reveal_type(x): # revealed: Literal[42]
|
||||
pass
|
||||
case 6.0 if reveal_type(x): # revealed: float
|
||||
pass
|
||||
case 1j if reveal_type(x): # revealed: complex
|
||||
pass
|
||||
case b"foo" if reveal_type(x): # revealed: Literal[b"foo"]
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Or patterns
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo" | 42 | None:
|
||||
reveal_type(x) # revealed: Literal["foo", 42] | None
|
||||
case "foo" | tuple():
|
||||
reveal_type(x) # revealed: Literal["foo"] | tuple
|
||||
case True | False:
|
||||
reveal_type(x) # revealed: bool
|
||||
case 3.14 | 2.718 | 1.414:
|
||||
reveal_type(x) # revealed: float
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
## Or patterns with guard
|
||||
|
||||
```py
|
||||
def get_object() -> object:
|
||||
return object()
|
||||
|
||||
x = get_object()
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
match x:
|
||||
case "foo" | 42 | None if reveal_type(x): # revealed: Literal["foo", 42] | None
|
||||
pass
|
||||
case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple
|
||||
pass
|
||||
case True | False if reveal_type(x): # revealed: bool
|
||||
pass
|
||||
case 3.14 | 2.718 | 1.414 if reveal_type(x): # revealed: float
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: object
|
||||
```
|
||||
|
||||
@@ -76,9 +76,6 @@ No narrowing should occur if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# The following diagnostic is valid, since the three-argument form of `type`
|
||||
# can only be called with `str` as the first argument.
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
|
||||
@@ -28,7 +28,10 @@ def f() -> None:
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__value__) # revealed: Any
|
||||
# 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(@property)
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
@@ -71,7 +74,7 @@ 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(full tuple[...] support)
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## `TypeAliasType` properties
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
# Properties
|
||||
|
||||
`property` is a built-in class in Python that can be used to model class attributes with custom
|
||||
getters, setters, and deleters.
|
||||
|
||||
## Basic getter
|
||||
|
||||
`property` is typically used as a decorator on a getter method. It turns the method into a property
|
||||
object. When accessing the property on an instance, the descriptor protocol is invoked, which calls
|
||||
the getter method:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(C().my_property) # revealed: int
|
||||
```
|
||||
|
||||
When a property is accessed on the class directly, the descriptor protocol is also invoked, but
|
||||
`property.__get__` simply returns itself in this case (when `instance` is `None`):
|
||||
|
||||
```py
|
||||
reveal_type(C.my_property) # revealed: property
|
||||
```
|
||||
|
||||
## Getter and setter
|
||||
|
||||
A property can also have a setter method, which is used to set the value of the property. The setter
|
||||
method is defined using the `@<property_name>.setter` decorator. The setter method takes the value
|
||||
to be set as an argument.
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
c = C()
|
||||
reveal_type(c.my_property) # revealed: int
|
||||
c.my_property = 2
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.my_property = "a"
|
||||
```
|
||||
|
||||
## `property.getter`
|
||||
|
||||
`property.getter` can be used to overwrite the getter method of a property. This does not overwrite
|
||||
the existing setter:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
@my_property.getter
|
||||
def my_property(self) -> str:
|
||||
return "a"
|
||||
|
||||
c = C()
|
||||
reveal_type(c.my_property) # revealed: str
|
||||
c.my_property = 2
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.my_property = "b"
|
||||
```
|
||||
|
||||
## `property.deleter`
|
||||
|
||||
We do not support `property.deleter` yet, but we make sure that it does not invalidate the getter or
|
||||
setter:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def my_property(self) -> int:
|
||||
return 1
|
||||
|
||||
@my_property.setter
|
||||
def my_property(self, value: int) -> None:
|
||||
pass
|
||||
|
||||
@my_property.deleter
|
||||
def my_property(self) -> None:
|
||||
pass
|
||||
|
||||
c = C()
|
||||
reveal_type(c.my_property) # revealed: int
|
||||
c.my_property = 2
|
||||
# error: [invalid-assignment]
|
||||
c.my_property = "a"
|
||||
```
|
||||
|
||||
## Failure cases
|
||||
|
||||
### Attempting to write to a read-only property
|
||||
|
||||
When attempting to write to a read-only property, we emit an error:
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: [invalid-assignment]
|
||||
c.attr = 2
|
||||
```
|
||||
|
||||
### Attempting to read a write-only property
|
||||
|
||||
When attempting to read a write-only property, we emit an error:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def attr_setter(self, value: int) -> None:
|
||||
pass
|
||||
attr = property(fset=attr_setter)
|
||||
|
||||
c = C()
|
||||
c.attr = 1
|
||||
|
||||
# TODO: An error should be emitted here, and the type should be `Unknown`
|
||||
# or `Never`. See https://github.com/astral-sh/ruff/issues/16298 for more
|
||||
# details.
|
||||
reveal_type(c.attr) # revealed: Unknown | property
|
||||
```
|
||||
|
||||
### Wrong setter signature
|
||||
|
||||
```py
|
||||
class C:
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return 1
|
||||
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 2 (`fset`) of bound method `setter`; expected type `(Any, Any, /) -> None`"
|
||||
@attr.setter
|
||||
def attr(self) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
### Wrong getter signature
|
||||
|
||||
```py
|
||||
class C:
|
||||
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 1 (`fget`) of class `property`; expected type `((Any, /) -> Any) | None`"
|
||||
@property
|
||||
def attr(self, x: int) -> int:
|
||||
return 1
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
### Manually constructed property
|
||||
|
||||
Properties can also be constructed manually using the `property` class. We partially support this:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def attr_getter(self) -> int:
|
||||
return 1
|
||||
attr = property(attr_getter)
|
||||
|
||||
c = C()
|
||||
reveal_type(c.attr) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
But note that we return `Unknown | int` because we did not declare the `attr` attribute. This is
|
||||
consistent with how we usually treat attributes, but here, if we try to declare `attr` as
|
||||
`property`, we fail to understand the property, since the `property` declaration shadows the more
|
||||
precise type that we infer for `property(attr_getter)` (which includes the actual information about
|
||||
the getter).
|
||||
|
||||
```py
|
||||
class C:
|
||||
def attr_getter(self) -> int:
|
||||
return 1
|
||||
attr: property = property(attr_getter)
|
||||
|
||||
c = C()
|
||||
reveal_type(c.attr) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Behind the scenes
|
||||
|
||||
In this section, we trace through some of the steps that make properties work. We start with a
|
||||
simple class `C` and a property `attr`:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
self._attr: int = 0
|
||||
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return self._attr
|
||||
|
||||
@attr.setter
|
||||
def attr(self, value: str) -> None:
|
||||
self._attr = len(value)
|
||||
```
|
||||
|
||||
Next, we create an instance of `C`. As we have seen above, accessing `attr` on the instance will
|
||||
return an `int`:
|
||||
|
||||
```py
|
||||
c = C()
|
||||
|
||||
reveal_type(c.attr) # revealed: int
|
||||
```
|
||||
|
||||
Behind the scenes, when we write `c.attr`, the first thing that happens is that we statically look
|
||||
up the symbol `attr` on the meta-type of `c`, i.e. the class `C`. We can emulate this static lookup
|
||||
using `inspect.getattr_static`, to see that `attr` is actually an instance of the `property` class:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
attr_property = getattr_static(C, "attr")
|
||||
reveal_type(attr_property) # revealed: property
|
||||
```
|
||||
|
||||
The `property` class has a `__get__` method, which makes it a descriptor. It also has a `__set__`
|
||||
method, which means that it is a *data* descriptor (if there is no setter, `__set__` is still
|
||||
available but yields an `AttributeError` at runtime).
|
||||
|
||||
```py
|
||||
reveal_type(type(attr_property).__get__) # revealed: <wrapper-descriptor `__get__` of `property` objects>
|
||||
reveal_type(type(attr_property).__set__) # revealed: <wrapper-descriptor `__set__` of `property` objects>
|
||||
```
|
||||
|
||||
When we access `c.attr`, the `__get__` method of the `property` class is called, passing the
|
||||
property object itself as the first argument, and the class instance `c` as the second argument. The
|
||||
third argument is the "owner" which can be set to `None` or to `C` in this case:
|
||||
|
||||
```py
|
||||
reveal_type(type(attr_property).__get__(attr_property, c, C)) # revealed: int
|
||||
reveal_type(type(attr_property).__get__(attr_property, c, None)) # revealed: int
|
||||
```
|
||||
|
||||
Alternatively, the above can also be written as a method call:
|
||||
|
||||
```py
|
||||
reveal_type(attr_property.__get__(c, C)) # revealed: int
|
||||
```
|
||||
|
||||
When we access `attr` on the class itself, the descriptor protocol is also invoked, but the instance
|
||||
argument is set to `None`. When `instance` is `None`, the call to `property.__get__` returns the
|
||||
property instance itself. So the following expressions are all equivalent
|
||||
|
||||
```py
|
||||
reveal_type(attr_property) # revealed: property
|
||||
reveal_type(C.attr) # revealed: property
|
||||
reveal_type(attr_property.__get__(None, C)) # revealed: property
|
||||
reveal_type(type(attr_property).__get__(attr_property, None, C)) # revealed: property
|
||||
```
|
||||
|
||||
When we set the property using `c.attr = "a"`, the `__set__` method of the property class is called.
|
||||
This attribute access desugars to
|
||||
|
||||
```py
|
||||
type(attr_property).__set__(attr_property, c, "a")
|
||||
|
||||
# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed"
|
||||
type(attr_property).__set__(attr_property, c, 1)
|
||||
```
|
||||
|
||||
which is also equivalent to the following expressions:
|
||||
|
||||
```py
|
||||
attr_property.__set__(c, "a")
|
||||
# error: [call-non-callable]
|
||||
attr_property.__set__(c, 1)
|
||||
|
||||
C.attr.__set__(c, "a")
|
||||
# error: [call-non-callable]
|
||||
C.attr.__set__(c, 1)
|
||||
```
|
||||
|
||||
Properties also have `fget` and `fset` attributes that can be used to retrieve the original getter
|
||||
and setter functions, respectively.
|
||||
|
||||
```py
|
||||
reveal_type(attr_property.fget) # revealed: Literal[attr]
|
||||
reveal_type(attr_property.fget(c)) # revealed: int
|
||||
|
||||
reveal_type(attr_property.fset) # revealed: Literal[attr]
|
||||
reveal_type(attr_property.fset(c, "a")) # revealed: None
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
attr_property.fset(c, 1)
|
||||
```
|
||||
@@ -154,10 +154,6 @@ x = 1
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
[y for a in range(1)]
|
||||
y = 1
|
||||
```
|
||||
|
||||
### Set comprehensions
|
||||
@@ -169,10 +165,6 @@ x = 1
|
||||
{reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
{y for a in range(1)}
|
||||
y = 1
|
||||
```
|
||||
|
||||
### Dict comprehensions
|
||||
@@ -184,10 +176,6 @@ x = 1
|
||||
{a: reveal_type(x) for a in range(1)}
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
{a: y for a in range(1)}
|
||||
y = 1
|
||||
```
|
||||
|
||||
### Generator expressions
|
||||
@@ -199,10 +187,6 @@ x = 1
|
||||
list(reveal_type(x) for a in range(1))
|
||||
|
||||
x = 2
|
||||
|
||||
# error: [unresolved-reference]
|
||||
list(y for a in range(1))
|
||||
y = 1
|
||||
```
|
||||
|
||||
`evaluated_later.py`:
|
||||
@@ -278,14 +262,6 @@ def _():
|
||||
[reveal_type(x) for a in range(1)]
|
||||
|
||||
x = 2
|
||||
|
||||
x = 1
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# revealed: Unknown | Literal[1]
|
||||
[reveal_type(x) for _ in [1]]
|
||||
x = 2
|
||||
```
|
||||
|
||||
### Eager scope within a lazy scope
|
||||
|
||||
@@ -58,8 +58,9 @@ reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
# TODO: needs support generics; should be `dict[str, Any]`:
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(generics)
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
@@ -91,9 +92,10 @@ reveal_type(__dict__) # revealed: Literal["foo"]
|
||||
import foo
|
||||
from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support generics; should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(generics)
|
||||
reveal_type(foo_dict) # revealed: @Todo(generics)
|
||||
# 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(@property)
|
||||
reveal_type(foo_dict) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
@@ -12,12 +12,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__: None = None
|
||||
3 |
|
||||
4 | class A:
|
||||
5 | def __eq__(self, other) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
1 | class A:
|
||||
2 | def __eq__(self, other) -> NotBoolable:
|
||||
3 | return NotBoolable()
|
||||
4 |
|
||||
5 | class NotBoolable:
|
||||
6 | __bool__: None = None
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
|
||||
@@ -25,7 +25,7 @@ reveal_type(y) # revealed: Unknown
|
||||
def _(n: int):
|
||||
a = b"abcde"[n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -44,10 +44,10 @@ b[::0] # error: [zero-stepsize-in-slice]
|
||||
def _(m: int, n: int):
|
||||
byte_slice1 = b[m:n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
def _(s: bytes) -> bytes:
|
||||
byte_slice2 = s[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
return reveal_type(byte_slice2) # revealed: @Todo(return type of overloaded function)
|
||||
return reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
@@ -12,13 +12,13 @@ x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x[0]) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Assignments within list assignment
|
||||
|
||||
@@ -22,7 +22,7 @@ reveal_type(b) # revealed: Unknown
|
||||
def _(n: int):
|
||||
a = "abcde"[n]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -76,11 +76,11 @@ def _(m: int, n: int, s2: str):
|
||||
|
||||
substring1 = s[m:n]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(substring1) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
substring2 = s2[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(substring2) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
@@ -70,7 +70,7 @@ def _(m: int, n: int):
|
||||
|
||||
tuple_slice = t[m:n]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type of overloaded function)
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
@@ -48,8 +48,6 @@ from typing import no_type_check
|
||||
@unknown_decorator # error: [unresolved-reference]
|
||||
@no_type_check
|
||||
def test() -> int:
|
||||
# TODO: this should not be an error
|
||||
# error: [unresolved-reference]
|
||||
return a + 5
|
||||
```
|
||||
|
||||
@@ -66,8 +64,6 @@ from typing import no_type_check
|
||||
@no_type_check
|
||||
@unknown_decorator
|
||||
def test() -> int:
|
||||
# TODO: this should not be an error
|
||||
# error: [unresolved-reference]
|
||||
return a + 5
|
||||
```
|
||||
|
||||
|
||||
@@ -121,9 +121,9 @@ But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` unti
|
||||
properties on instance types:
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info.micro) # revealed: int
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
reveal_type(sys.version_info.serial) # revealed: int
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## Accessing fields by index/slice
|
||||
|
||||
@@ -654,7 +654,9 @@ def f(cond: bool) -> str:
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
return "a"
|
||||
x = "after-return"
|
||||
reveal_type(x) # revealed: Never
|
||||
# TODO: no unresolved-reference error
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
else:
|
||||
x = "else"
|
||||
return reveal_type(x) # revealed: Literal["else"]
|
||||
|
||||
@@ -398,16 +398,16 @@ def f(x: TypeOf) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `CallableTypeOf`
|
||||
## `CallableTypeFromFunction`
|
||||
|
||||
The `CallableTypeOf` special form can be used to extract the `Callable` structural type inhabited by
|
||||
a given callable object. This can be used to get the externally visibly signature of the object,
|
||||
which can then be used to test various type properties.
|
||||
The `CallableTypeFromFunction` special form can be used to extract the type of a function literal as
|
||||
a callable type. This can be used to get the externally-visibly signature of the function, which can
|
||||
then be used to test various type properties.
|
||||
|
||||
It accepts a single type parameter which is expected to be a callable object.
|
||||
It accepts a single type parameter which is expected to be a function literal.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf
|
||||
from knot_extensions import CallableTypeFromFunction
|
||||
|
||||
def f1():
|
||||
return
|
||||
@@ -418,41 +418,25 @@ def f2() -> int:
|
||||
def f3(x: int, y: str) -> None:
|
||||
return
|
||||
|
||||
# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeOf` expected exactly one type parameter"
|
||||
c1: CallableTypeOf[f1, f2]
|
||||
# error: [invalid-type-form] "Special form `knot_extensions.CallableTypeFromFunction` expected exactly one type parameter"
|
||||
c1: CallableTypeFromFunction[f1, f2]
|
||||
# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeFromFunction` to be a function literal, but got `Literal[int]`"
|
||||
c2: CallableTypeFromFunction[int]
|
||||
|
||||
# error: [invalid-type-form] "Expected the first argument to `knot_extensions.CallableTypeOf` to be a callable object, but got an object of type `Literal["foo"]`"
|
||||
c2: CallableTypeOf["foo"]
|
||||
|
||||
# error: [invalid-type-form] "`knot_extensions.CallableTypeOf` requires exactly one argument when used in a type expression"
|
||||
def f(x: CallableTypeOf) -> None:
|
||||
# error: [invalid-type-form] "`knot_extensions.CallableTypeFromFunction` requires exactly one argument when used in a type expression"
|
||||
def f(x: CallableTypeFromFunction) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
Using it in annotation to reveal the signature of the callable object:
|
||||
Using it in annotation to reveal the signature of the function:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __init__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
def __call__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
def _(
|
||||
c1: CallableTypeOf[f1],
|
||||
c2: CallableTypeOf[f2],
|
||||
c3: CallableTypeOf[f3],
|
||||
c4: CallableTypeOf[Foo],
|
||||
c5: CallableTypeOf[Foo(42).__call__],
|
||||
c1: CallableTypeFromFunction[f1],
|
||||
c2: CallableTypeFromFunction[f2],
|
||||
c3: CallableTypeFromFunction[f3],
|
||||
) -> None:
|
||||
reveal_type(c1) # revealed: () -> Unknown
|
||||
reveal_type(c2) # revealed: () -> int
|
||||
reveal_type(c3) # revealed: (x: int, y: str) -> None
|
||||
|
||||
# TODO: should be `(x: int) -> Foo`
|
||||
reveal_type(c4) # revealed: (...) -> Foo
|
||||
|
||||
# TODO: `self` is bound here; this should probably be `(x: int) -> str`?
|
||||
reveal_type(c5) # revealed: (self, x: int) -> str
|
||||
```
|
||||
|
||||
@@ -194,9 +194,6 @@ static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
|
||||
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
|
||||
static_assert(is_assignable_to(tuple[()], tuple))
|
||||
static_assert(is_assignable_to(tuple[int, str], tuple))
|
||||
static_assert(is_assignable_to(tuple[Any], tuple))
|
||||
|
||||
static_assert(not is_assignable_to(tuple[()], tuple[int]))
|
||||
static_assert(not is_assignable_to(tuple[int], tuple[str]))
|
||||
@@ -405,7 +402,7 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable).
|
||||
### Return type
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, Unknown, static_assert, is_assignable_to
|
||||
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
|
||||
@@ -435,7 +432,7 @@ A `Callable` which uses the gradual form (`...`) for the parameter types is cons
|
||||
input signature.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, static_assert, is_assignable_to
|
||||
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
|
||||
@@ -453,12 +450,12 @@ def keyword_only(*, a: int, b: int) -> None: ...
|
||||
def keyword_variadic(**kwargs: int) -> None: ...
|
||||
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
And, even if the parameters are unannotated.
|
||||
@@ -471,29 +468,12 @@ def keyword_only(*, a, b) -> None: ...
|
||||
def keyword_variadic(**kwargs) -> None: ...
|
||||
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
|
||||
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeOf[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
### Function types
|
||||
|
||||
```py
|
||||
from typing import Any, Callable
|
||||
|
||||
def f(x: Any) -> str:
|
||||
return ""
|
||||
|
||||
def g(x: Any) -> int:
|
||||
return 1
|
||||
|
||||
c: Callable[[Any], str] = f
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[g]` is not assignable to `(Any, /) -> str`"
|
||||
c: Callable[[Any], str] = g
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
|
||||
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
|
||||
```
|
||||
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
|
||||
@@ -61,7 +61,7 @@ static_assert(is_disjoint_from(B2, FinalSubclass))
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, Never
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(tuple[()], TypeOf[object]))
|
||||
@@ -353,41 +353,3 @@ class UsesMeta2(metaclass=Meta2): ...
|
||||
|
||||
static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2]))
|
||||
```
|
||||
|
||||
## Callables
|
||||
|
||||
No two callable types are disjoint because there exists a non-empty callable type
|
||||
`(*args: object, **kwargs: object) -> Never` that is a subtype of all fully static callable types.
|
||||
As such, for any two callable types, it is possible to conceive of a runtime callable object that
|
||||
would inhabit both types simultaneously.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert
|
||||
from typing_extensions import Callable, Literal, Never
|
||||
|
||||
def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(not is_disjoint_from(Callable[[], Never], CallableTypeOf[mixed]))
|
||||
static_assert(not is_disjoint_from(Callable[[int, str], float], CallableTypeOf[mixed]))
|
||||
|
||||
# Using gradual form
|
||||
static_assert(not is_disjoint_from(Callable[..., None], Callable[[], None]))
|
||||
static_assert(not is_disjoint_from(Callable[..., None], Callable[..., None]))
|
||||
static_assert(not is_disjoint_from(Callable[..., None], Callable[[Literal[1]], None]))
|
||||
|
||||
# Using `Never`
|
||||
static_assert(not is_disjoint_from(Callable[[], Never], Callable[[], Never]))
|
||||
static_assert(not is_disjoint_from(Callable[[Never], str], Callable[[Never], int]))
|
||||
```
|
||||
|
||||
A callable type is disjoint from all literal types.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert
|
||||
from typing_extensions import Callable, Literal, Never
|
||||
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[""]))
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[b""]))
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[1]))
|
||||
static_assert(is_disjoint_from(Callable[[], None], Literal[True]))
|
||||
```
|
||||
|
||||
@@ -127,14 +127,13 @@ the parameter in one of the callable has a default value then the corresponding
|
||||
other callable should also have a default value.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
|
||||
from typing import Callable
|
||||
|
||||
def f1(a: int = 1) -> None: ...
|
||||
def f2(a: int = 2) -> None: ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1] | bool | CallableTypeOf[f2], CallableTypeOf[f2] | bool | CallableTypeOf[f1]))
|
||||
static_assert(is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
```
|
||||
|
||||
The names of the positional-only, variadic and keyword-variadic parameters does not need to be the
|
||||
@@ -144,8 +143,7 @@ same.
|
||||
def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ...
|
||||
def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]))
|
||||
static_assert(is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
```
|
||||
|
||||
Putting it all together, the following two callables are equivalent:
|
||||
@@ -154,8 +152,7 @@ Putting it all together, the following two callables are equivalent:
|
||||
def f5(a1: int, /, b: float, c: bool = False, *args1: int, d: int = 1, e: str, **kwargs1: float) -> None: ...
|
||||
def f6(a2: int, /, b: float, c: bool = True, *args2: int, d: int = 2, e: str, **kwargs2: float) -> None: ...
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], CallableTypeOf[f6] | bool | CallableTypeOf[f5]))
|
||||
static_assert(is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
### Not equivalent
|
||||
@@ -163,7 +160,7 @@ static_assert(is_equivalent_to(CallableTypeOf[f5] | bool | CallableTypeOf[f6], C
|
||||
There are multiple cases when two callable types are not equivalent which are enumerated below.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_equivalent_to, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
|
||||
from typing import Callable
|
||||
```
|
||||
|
||||
@@ -173,7 +170,7 @@ When the number of parameters is different:
|
||||
def f1(a: int) -> None: ...
|
||||
def f2(a: int, b: int) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
```
|
||||
|
||||
When either of the callable types uses a gradual form for the parameters:
|
||||
@@ -190,9 +187,9 @@ def f3(): ...
|
||||
def f4() -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f4], CallableTypeOf[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f3]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f4], CallableTypeFromFunction[f3]))
|
||||
```
|
||||
|
||||
When the parameter names are different:
|
||||
@@ -201,13 +198,13 @@ When the parameter names are different:
|
||||
def f5(a: int) -> None: ...
|
||||
def f6(b: int) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f5], CallableTypeOf[f6]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
When only one of the callable types has parameter names:
|
||||
|
||||
```py
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f5], Callable[[int], None]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], Callable[[int], None]))
|
||||
```
|
||||
|
||||
When the parameter kinds are different:
|
||||
@@ -216,7 +213,7 @@ When the parameter kinds are different:
|
||||
def f7(a: int, /) -> None: ...
|
||||
def f8(a: int) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f7], CallableTypeOf[f8]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f7], CallableTypeFromFunction[f8]))
|
||||
```
|
||||
|
||||
When the annotated types of the parameters are not equivalent or absent in one or both of the
|
||||
@@ -227,10 +224,10 @@ def f9(a: int) -> None: ...
|
||||
def f10(a: str) -> None: ...
|
||||
def f11(a) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f9], CallableTypeOf[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f10], CallableTypeOf[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f11], CallableTypeOf[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f9], CallableTypeFromFunction[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f10], CallableTypeFromFunction[f11]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f10]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f11]))
|
||||
```
|
||||
|
||||
When the default value for a parameter is present only in one of the callable type:
|
||||
@@ -239,19 +236,8 @@ When the default value for a parameter is present only in one of the callable ty
|
||||
def f12(a: int) -> None: ...
|
||||
def f13(a: int = 2) -> None: ...
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f12], CallableTypeOf[f13]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[f13], CallableTypeOf[f12]))
|
||||
```
|
||||
|
||||
### Unions containing `Callable`s containing unions
|
||||
|
||||
Differently ordered unions inside `Callable`s inside unions can still be equivalent:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f12], CallableTypeFromFunction[f13]))
|
||||
static_assert(not is_equivalent_to(CallableTypeFromFunction[f13], CallableTypeFromFunction[f12]))
|
||||
```
|
||||
|
||||
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
|
||||
|
||||
@@ -80,7 +80,7 @@ Using function literals, we can check more variations of callable types as it al
|
||||
parameters without annotations and no return type.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_fully_static, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_fully_static, static_assert
|
||||
|
||||
def f00() -> None: ...
|
||||
def f01(a: int, b: str) -> None: ...
|
||||
@@ -90,12 +90,12 @@ def f13(a, b: int): ...
|
||||
def f14(a, b: int) -> None: ...
|
||||
def f15(a, b) -> None: ...
|
||||
|
||||
static_assert(is_fully_static(CallableTypeOf[f00]))
|
||||
static_assert(is_fully_static(CallableTypeOf[f01]))
|
||||
static_assert(is_fully_static(CallableTypeFromFunction[f00]))
|
||||
static_assert(is_fully_static(CallableTypeFromFunction[f01]))
|
||||
|
||||
static_assert(not is_fully_static(CallableTypeOf[f11]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f12]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f13]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f14]))
|
||||
static_assert(not is_fully_static(CallableTypeOf[f15]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f11]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f12]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f13]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f14]))
|
||||
static_assert(not is_fully_static(CallableTypeFromFunction[f15]))
|
||||
```
|
||||
|
||||
@@ -73,7 +73,7 @@ gradual types. The cases with fully static types and using different combination
|
||||
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, CallableTypeOf, is_gradual_equivalent_to, static_assert
|
||||
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
|
||||
@@ -92,7 +92,7 @@ type of `Any`.
|
||||
def f1():
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[], Any]))
|
||||
```
|
||||
|
||||
And, similarly for parameters with no annotations.
|
||||
@@ -101,7 +101,7 @@ And, similarly for parameters with no annotations.
|
||||
def f2(a, b, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
|
||||
```
|
||||
|
||||
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
|
||||
@@ -115,8 +115,8 @@ def variadic_without_annotation(*args, **kwargs):
|
||||
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||
return
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
|
||||
```
|
||||
|
||||
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||
@@ -129,8 +129,8 @@ def variadic_args(*args):
|
||||
def variadic_kwargs(**kwargs):
|
||||
return
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
|
||||
```
|
||||
|
||||
Parameter names, default values, and it's kind should also be considered when checking for gradual
|
||||
@@ -140,21 +140,18 @@ equivalence.
|
||||
def f1(a): ...
|
||||
def f2(b): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f2]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||
|
||||
def f3(a=1): ...
|
||||
def f4(a=2): ...
|
||||
def f5(a): ...
|
||||
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4]))
|
||||
static_assert(
|
||||
is_gradual_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3])
|
||||
)
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f5]))
|
||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
|
||||
|
||||
def f6(a, /): ...
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
|
||||
```
|
||||
|
||||
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
A type is single-valued iff it is not empty and all inhabitants of it compare equal.
|
||||
|
||||
```py
|
||||
import types
|
||||
from typing_extensions import Any, Literal, LiteralString, Never, Callable
|
||||
from knot_extensions import is_single_valued, static_assert, TypeOf
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
static_assert(is_single_valued(Literal[True]))
|
||||
@@ -26,11 +25,4 @@ static_assert(not is_single_valued(tuple[None, int]))
|
||||
|
||||
static_assert(not is_single_valued(Callable[..., None]))
|
||||
static_assert(not is_single_valued(Callable[[int, str], None]))
|
||||
|
||||
class A:
|
||||
def method(self): ...
|
||||
|
||||
static_assert(is_single_valued(TypeOf[A().method]))
|
||||
static_assert(is_single_valued(TypeOf[types.FunctionType.__get__]))
|
||||
static_assert(is_single_valued(TypeOf[A.method.__get__]))
|
||||
```
|
||||
|
||||
@@ -72,6 +72,7 @@ python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(Ellipsis.__class__))
|
||||
@@ -94,68 +95,3 @@ from knot_extensions import static_assert, is_singleton
|
||||
|
||||
static_assert(is_singleton(types.EllipsisType))
|
||||
```
|
||||
|
||||
## `builtins.NotImplemented` / `types.NotImplementedType`
|
||||
|
||||
### All Python versions
|
||||
|
||||
Just like `Ellipsis`, the type of `NotImplemented` was not exposed on Python \<3.10. However, we
|
||||
still recognize the type as a singleton in all Python versions.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(NotImplemented.__class__))
|
||||
```
|
||||
|
||||
### Python 3.10+
|
||||
|
||||
On Python 3.10+, the standard library exposes the type of `NotImplemented` as
|
||||
`types.NotImplementedType`. We also recognize this as a singleton type when it is referenced
|
||||
directly:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
import types
|
||||
from knot_extensions import static_assert, is_singleton
|
||||
|
||||
# TODO: types.NotImplementedType is a TypeAlias of builtins._NotImplementedType
|
||||
# Once TypeAlias support is added, it should satisfy `is_singleton`
|
||||
reveal_type(types.NotImplementedType) # revealed: Unknown | Literal[_NotImplementedType]
|
||||
static_assert(not is_singleton(types.NotImplementedType))
|
||||
```
|
||||
|
||||
### Callables
|
||||
|
||||
We currently treat the type of `types.FunctionType.__get__` as a singleton type that has its own
|
||||
dedicated variant in the `Type` enum. That variant should be understood as a singleton type, but the
|
||||
similar variants `Type::BoundMethod` and `Type::MethodWrapperDunderGet` should not be; nor should
|
||||
`Type::Callable` types.
|
||||
|
||||
If we refactor `Type` in the future to get rid of some or all of these `Type` variants, the
|
||||
assertion that the type of `types.FunctionType.__get__` is a singleton type does not necessarily
|
||||
have to hold true; it's more of a unit test for our current implementation.
|
||||
|
||||
```py
|
||||
import types
|
||||
from typing import Callable
|
||||
from knot_extensions import static_assert, is_singleton, TypeOf
|
||||
|
||||
class A:
|
||||
def method(self): ...
|
||||
|
||||
static_assert(is_singleton(TypeOf[types.FunctionType.__get__]))
|
||||
|
||||
static_assert(not is_singleton(Callable[[], None]))
|
||||
static_assert(not is_singleton(TypeOf[A().method]))
|
||||
static_assert(not is_singleton(TypeOf[A.method.__get__]))
|
||||
```
|
||||
|
||||
@@ -530,13 +530,13 @@ Parameter types are contravariant.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
|
||||
|
||||
def float_param(a: float, /) -> None: ...
|
||||
def int_param(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[int_param], Callable[[int], None]))
|
||||
static_assert(is_subtype_of(TypeOf[float_param], Callable[[float], None]))
|
||||
@@ -550,8 +550,8 @@ Parameter name is not required to be the same for positional-only parameters at
|
||||
```py
|
||||
def int_param_different_name(b: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_param_different_name]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_param_different_name], CallableTypeOf[int_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_param_different_name]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_param_different_name], CallableTypeFromFunction[int_param]))
|
||||
```
|
||||
|
||||
Multiple positional-only parameters are checked in order:
|
||||
@@ -560,8 +560,8 @@ Multiple positional-only parameters are checked in order:
|
||||
def multi_param1(a: float, b: int, c: str, /) -> None: ...
|
||||
def multi_param2(b: int, c: bool, a: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[multi_param1], Callable[[float, int, str], None]))
|
||||
|
||||
@@ -575,17 +575,17 @@ corresponding position in the supertype does not need to have a default value.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf
|
||||
|
||||
def float_with_default(a: float = 1, /) -> None: ...
|
||||
def int_with_default(a: int = 1, /) -> None: ...
|
||||
def int_without_default(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_without_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[int], None]))
|
||||
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[], None]))
|
||||
@@ -600,9 +600,9 @@ As the parameter itself is optional, it can be omitted in the supertype:
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_without_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
The subtype can include any number of positional-only parameters as long as they have the default
|
||||
@@ -611,8 +611,8 @@ value:
|
||||
```py
|
||||
def multi_param(a: float = 1, b: int = 2, c: str = "3", /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param]))
|
||||
```
|
||||
|
||||
#### Positional-only with other kinds
|
||||
@@ -621,7 +621,7 @@ If a parameter is declared as positional-only, then the corresponding parameter
|
||||
cannot be any other parameter kind.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def positional_only(a: int, /) -> None: ...
|
||||
def standard(a: int) -> None: ...
|
||||
@@ -629,10 +629,10 @@ def keyword_only(*, a: int) -> None: ...
|
||||
def variadic(*a: int) -> None: ...
|
||||
def keyword_variadic(**a: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[positional_only], CallableTypeOf[keyword_variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[positional_only], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Standard
|
||||
@@ -642,13 +642,13 @@ A standard parameter is either a positional or a keyword parameter.
|
||||
Unlike positional-only parameters, standard parameters should have the same name in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def int_param_a(a: int) -> None: ...
|
||||
def int_param_b(b: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param_a], CallableTypeOf[int_param_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param_b], CallableTypeOf[int_param_a]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_a], CallableTypeFromFunction[int_param_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param_b], CallableTypeFromFunction[int_param_a]))
|
||||
```
|
||||
|
||||
Apart from the name, it behaves the same as positional-only parameters.
|
||||
@@ -657,8 +657,8 @@ Apart from the name, it behaves the same as positional-only parameters.
|
||||
def float_param(a: float) -> None: ...
|
||||
def int_param(a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_param], CallableTypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[float_param]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))
|
||||
```
|
||||
|
||||
With the same rules for default values as well.
|
||||
@@ -668,14 +668,14 @@ def float_with_default(a: float = 1) -> None: ...
|
||||
def int_with_default(a: int = 1) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_param], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_param]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
Multiple standard parameters are checked in order along with their names:
|
||||
@@ -684,8 +684,8 @@ Multiple standard parameters are checked in order along with their names:
|
||||
def multi_param1(a: float, b: int, c: str) -> None: ...
|
||||
def multi_param2(a: int, b: bool, c: str) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param1], CallableTypeOf[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[multi_param2], CallableTypeOf[multi_param1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))
|
||||
```
|
||||
|
||||
The subtype can include as many standard parameters as long as they have the default value:
|
||||
@@ -693,8 +693,8 @@ The subtype can include as many standard parameters as long as they have the def
|
||||
```py
|
||||
def multi_param_default(a: float = 1, b: int = 2, c: str = "s") -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_param_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[multi_param_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_param_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[multi_param_default]))
|
||||
```
|
||||
|
||||
#### Standard with keyword-only
|
||||
@@ -704,26 +704,26 @@ parameter in the subtype with the same name. This is because a standard paramete
|
||||
than a keyword-only parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard_a(a: int) -> None: ...
|
||||
def keyword_b(*, b: int) -> None: ...
|
||||
|
||||
# The name of the parameters are different
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[keyword_b]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[keyword_b]))
|
||||
|
||||
def standard_float(a: float) -> None: ...
|
||||
def keyword_int(*, a: int) -> None: ...
|
||||
|
||||
# Here, the name of the parameters are the same
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[keyword_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[keyword_int]))
|
||||
|
||||
def standard_with_default(a: int = 1) -> None: ...
|
||||
def keyword_with_default(*, a: int = 1) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keyword_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keyword_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
|
||||
```
|
||||
|
||||
The position of the keyword-only parameters does not matter:
|
||||
@@ -732,7 +732,7 @@ The position of the keyword-only parameters does not matter:
|
||||
def multi_standard(a: float, b: int, c: str) -> None: ...
|
||||
def multi_keyword(*, b: bool, c: str, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_keyword]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_keyword]))
|
||||
```
|
||||
|
||||
#### Standard with positional-only
|
||||
@@ -742,25 +742,25 @@ parameter in the subtype at the same position. This is because a standard parame
|
||||
than a positional-only parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard_a(a: int) -> None: ...
|
||||
def positional_b(b: int, /) -> None: ...
|
||||
|
||||
# The names are not important in this context
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_a], CallableTypeOf[positional_b]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_a], CallableTypeFromFunction[positional_b]))
|
||||
|
||||
def standard_float(a: float) -> None: ...
|
||||
def positional_int(a: int, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_float], CallableTypeOf[positional_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_float], CallableTypeFromFunction[positional_int]))
|
||||
|
||||
def standard_with_default(a: int = 1) -> None: ...
|
||||
def positional_with_default(a: int = 1, /) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[positional_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[empty]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[positional_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[empty]))
|
||||
```
|
||||
|
||||
The position of the positional-only parameters matter:
|
||||
@@ -772,8 +772,8 @@ def multi_positional1(b: int, c: bool, a: str, /) -> None: ...
|
||||
# Here, the type of the parameter `a` makes the subtype relation invalid
|
||||
def multi_positional2(b: int, a: float, c: str, /) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional1]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[multi_standard], CallableTypeOf[multi_positional2]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_standard], CallableTypeFromFunction[multi_positional2]))
|
||||
```
|
||||
|
||||
#### Standard with variadic
|
||||
@@ -782,14 +782,14 @@ A variadic or keyword-variadic parameter in the supertype cannot be substituted
|
||||
parameter in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def standard(a: int) -> None: ...
|
||||
def variadic(*a: int) -> None: ...
|
||||
def keyword_variadic(**a: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword_variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[variadic]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Variadic
|
||||
@@ -797,13 +797,13 @@ static_assert(not is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keyword
|
||||
The name of the variadic parameter does not need to be the same in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic_float(*args2: float) -> None: ...
|
||||
def variadic_int(*args1: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_float], CallableTypeOf[variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[variadic_float]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_float], CallableTypeFromFunction[variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[variadic_float]))
|
||||
```
|
||||
|
||||
The variadic parameter does not need to be present in the supertype:
|
||||
@@ -811,8 +811,8 @@ The variadic parameter does not need to be present in the supertype:
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_int], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[variadic_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic_int], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[variadic_int]))
|
||||
```
|
||||
|
||||
#### Variadic with positional-only
|
||||
@@ -821,7 +821,7 @@ If the subtype has a variadic parameter then any unmatched positional-only param
|
||||
supertype should be checked against the variadic parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic(a: int, /, *args: float) -> None: ...
|
||||
|
||||
@@ -831,8 +831,8 @@ def positional_only(a: int, b: float, c: int, /) -> None: ...
|
||||
# Here, the parameter `b` is unmatched and there's also a variadic parameter
|
||||
def positional_variadic(a: int, b: float, /, *args: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_only]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[positional_variadic]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_only]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[positional_variadic]))
|
||||
```
|
||||
|
||||
#### Variadic with other kinds
|
||||
@@ -841,7 +841,7 @@ Variadic parameter in a subtype can only be used to match against an unmatched p
|
||||
parameters from the supertype, not any other parameter kind.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def variadic(*args: int) -> None: ...
|
||||
|
||||
@@ -853,55 +853,9 @@ def standard(a: int, b: float, /, c: int) -> None: ...
|
||||
def keyword_only(a: int, /, *, b: int) -> None: ...
|
||||
def keyword_variadic(a: int, /, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic], CallableTypeOf[keyword_variadic]))
|
||||
```
|
||||
|
||||
But, there are special cases when matching against standard parameters. This is due to the fact that
|
||||
a standard parameter can be passed as a positional or keyword parameter. This means that the
|
||||
subtyping relation needs to consider both cases.
|
||||
|
||||
```py
|
||||
def variadic_keyword(*args: int, **kwargs: int) -> None: ...
|
||||
def standard_int(a: int) -> None: ...
|
||||
def standard_float(a: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_float]))
|
||||
```
|
||||
|
||||
If the type of either the variadic or keyword-variadic parameter is not a supertype of the standard
|
||||
parameter, then the subtyping relation is invalid.
|
||||
|
||||
```py
|
||||
def variadic_bool(*args: bool, **kwargs: int) -> None: ...
|
||||
def keyword_variadic_bool(*args: int, **kwargs: bool) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_bool], CallableTypeOf[standard_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_variadic_bool], CallableTypeOf[standard_int]))
|
||||
```
|
||||
|
||||
The standard parameter can follow a variadic parameter in the subtype.
|
||||
|
||||
```py
|
||||
def standard_variadic_int(a: int, *args: int) -> None: ...
|
||||
def standard_variadic_float(a: int, *args: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_keyword], CallableTypeOf[standard_variadic_float]))
|
||||
```
|
||||
|
||||
The keyword part of the standard parameter can be matched against keyword-only parameter with the
|
||||
same name if the keyword-variadic parameter is absent.
|
||||
|
||||
```py
|
||||
def variadic_a(*args: int, a: int) -> None: ...
|
||||
def variadic_b(*args: int, b: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[variadic_a], CallableTypeOf[standard_int]))
|
||||
# The parameter name is different
|
||||
static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[standard_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[standard]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[variadic], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
#### Keyword-only
|
||||
@@ -909,15 +863,15 @@ static_assert(not is_subtype_of(CallableTypeOf[variadic_b], CallableTypeOf[stand
|
||||
For keyword-only parameters, the name should be the same:
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def keyword_int(*, a: int) -> None: ...
|
||||
def keyword_float(*, a: float) -> None: ...
|
||||
def keyword_b(*, b: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[keyword_float], CallableTypeOf[keyword_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_float]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_int], CallableTypeOf[keyword_b]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_float], CallableTypeFromFunction[keyword_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_float]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_int], CallableTypeFromFunction[keyword_b]))
|
||||
```
|
||||
|
||||
But, the order of the keyword-only parameters is not required to be the same:
|
||||
@@ -926,28 +880,28 @@ But, the order of the keyword-only parameters is not required to be the same:
|
||||
def keyword_ab(*, a: float, b: float) -> None: ...
|
||||
def keyword_ba(*, b: int, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[keyword_ab], CallableTypeOf[keyword_ba]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keyword_ba], CallableTypeOf[keyword_ab]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[keyword_ab], CallableTypeFromFunction[keyword_ba]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keyword_ba], CallableTypeFromFunction[keyword_ab]))
|
||||
```
|
||||
|
||||
#### Keyword-only with default
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def float_with_default(*, a: float = 1) -> None: ...
|
||||
def int_with_default(*, a: int = 1) -> None: ...
|
||||
def int_keyword(*, a: int) -> None: ...
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[float_with_default], CallableTypeOf[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[float_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[float_with_default], CallableTypeFromFunction[int_with_default]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[float_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[int_with_default]))
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[int_with_default], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[int_with_default]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[int_with_default]))
|
||||
```
|
||||
|
||||
Keyword-only parameters with default values can be mixed with the ones without default values in any
|
||||
@@ -957,20 +911,20 @@ order:
|
||||
# A keyword-only parameter with a default value follows the one without a default value (it's valid)
|
||||
def mixed(*, b: int = 1, a: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[int_keyword], CallableTypeOf[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[int_keyword]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[int_keyword], CallableTypeFromFunction[mixed]))
|
||||
```
|
||||
|
||||
#### Keyword-only with standard
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def keywords1(*, a: int, b: int) -> None: ...
|
||||
def standard(b: float, a: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keywords1], CallableTypeOf[standard]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard], CallableTypeOf[keywords1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords1], CallableTypeFromFunction[standard]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard], CallableTypeFromFunction[keywords1]))
|
||||
```
|
||||
|
||||
The subtype can include additional standard parameters as long as it has the default value:
|
||||
@@ -979,8 +933,8 @@ The subtype can include additional standard parameters as long as it has the def
|
||||
def standard_with_default(b: float, a: float, c: float = 1) -> None: ...
|
||||
def standard_without_default(b: float, a: float, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[standard_without_default], CallableTypeOf[keywords1]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[standard_with_default], CallableTypeOf[keywords1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[standard_without_default], CallableTypeFromFunction[keywords1]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[standard_with_default], CallableTypeFromFunction[keywords1]))
|
||||
```
|
||||
|
||||
Here, we mix keyword-only parameters with standard parameters:
|
||||
@@ -989,8 +943,8 @@ Here, we mix keyword-only parameters with standard parameters:
|
||||
def keywords2(*, a: int, c: int, b: int) -> None: ...
|
||||
def mixed(b: float, a: float, *, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[keywords2], CallableTypeOf[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[keywords2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[keywords2], CallableTypeFromFunction[mixed]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
But, we shouldn't consider any unmatched positional-only parameters:
|
||||
@@ -998,7 +952,7 @@ But, we shouldn't consider any unmatched positional-only parameters:
|
||||
```py
|
||||
def mixed_positional(b: float, /, a: float, *, c: float) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[mixed_positional], CallableTypeOf[keywords2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[mixed_positional], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
But, an unmatched variadic parameter is still valid:
|
||||
@@ -1006,7 +960,7 @@ But, an unmatched variadic parameter is still valid:
|
||||
```py
|
||||
def mixed_variadic(*args: float, a: float, b: float, c: float, **kwargs: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywords2]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed_variadic], CallableTypeFromFunction[keywords2]))
|
||||
```
|
||||
|
||||
#### Keyword-variadic
|
||||
@@ -1014,13 +968,13 @@ static_assert(is_subtype_of(CallableTypeOf[mixed_variadic], CallableTypeOf[keywo
|
||||
The name of the keyword-variadic parameter does not need to be the same in the subtype.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def kwargs_float(**kwargs2: float) -> None: ...
|
||||
def kwargs_int(**kwargs1: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs_float], CallableTypeOf[kwargs_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[kwargs_float]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_float], CallableTypeFromFunction[kwargs_int]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[kwargs_float]))
|
||||
```
|
||||
|
||||
A variadic parameter can be omitted in the subtype:
|
||||
@@ -1028,8 +982,8 @@ A variadic parameter can be omitted in the subtype:
|
||||
```py
|
||||
def empty() -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs_int], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[kwargs_int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs_int], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[kwargs_int]))
|
||||
```
|
||||
|
||||
#### Keyword-variadic with keyword-only
|
||||
@@ -1038,14 +992,14 @@ If the subtype has a keyword-variadic parameter then any unmatched keyword-only
|
||||
supertype should be checked against the keyword-variadic parameter.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def kwargs(**kwargs: float) -> None: ...
|
||||
def keyword_only(*, a: int, b: float, c: bool) -> None: ...
|
||||
def keyword_variadic(*, a: int, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_only]))
|
||||
static_assert(is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[keyword_variadic]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_only]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[keyword_variadic]))
|
||||
```
|
||||
|
||||
This is valid only for keyword-only parameters, not any other parameter kind:
|
||||
@@ -1056,8 +1010,8 @@ def mixed1(a: int, *, b: int) -> None: ...
|
||||
# Same as above but with the default value
|
||||
def mixed2(a: int = 1, *, b: int) -> None: ...
|
||||
|
||||
static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed1]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[kwargs], CallableTypeOf[mixed2]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed1]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[kwargs], CallableTypeFromFunction[mixed2]))
|
||||
```
|
||||
|
||||
#### Empty
|
||||
@@ -1066,86 +1020,13 @@ When the supertype has an empty list of parameters, then the subtype can have an
|
||||
as long as they contain the default values for non-variadic parameters.
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
|
||||
|
||||
def empty() -> None: ...
|
||||
def mixed(a: int = 1, /, b: int = 2, *args: int, c: int = 3, **kwargs: int) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[mixed], CallableTypeOf[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeOf[empty], CallableTypeOf[mixed]))
|
||||
```
|
||||
|
||||
#### Object
|
||||
|
||||
```py
|
||||
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert, TypeOf
|
||||
from typing import Callable
|
||||
|
||||
def f1(a: int, b: str, /, *c: float, d: int = 1, **e: float) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(CallableTypeOf[f1], object))
|
||||
static_assert(not is_subtype_of(object, CallableTypeOf[f1]))
|
||||
|
||||
def _(
|
||||
f3: Callable[[int, str], None],
|
||||
) -> None:
|
||||
static_assert(is_subtype_of(TypeOf[f3], object))
|
||||
static_assert(not is_subtype_of(object, TypeOf[f3]))
|
||||
|
||||
class C:
|
||||
def foo(self) -> None: ...
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[C.foo], object))
|
||||
static_assert(not is_subtype_of(object, TypeOf[C.foo]))
|
||||
```
|
||||
|
||||
### Classes with `__call__`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert, is_assignable_to
|
||||
|
||||
class A:
|
||||
def __call__(self, a: int) -> int:
|
||||
return a
|
||||
|
||||
a = A()
|
||||
|
||||
static_assert(is_subtype_of(A, Callable[[int], int]))
|
||||
static_assert(not is_subtype_of(A, Callable[[], int]))
|
||||
static_assert(not is_subtype_of(Callable[[int], int], A))
|
||||
|
||||
def f(fn: Callable[[int], int]) -> None: ...
|
||||
|
||||
f(a)
|
||||
```
|
||||
|
||||
### Bound methods
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||
|
||||
class A:
|
||||
def f(self, a: int) -> int:
|
||||
return a
|
||||
|
||||
@classmethod
|
||||
def g(cls, a: int) -> int:
|
||||
return a
|
||||
|
||||
a = A()
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[a.f], Callable[[int], int]))
|
||||
static_assert(is_subtype_of(TypeOf[a.g], Callable[[int], int]))
|
||||
static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], int]))
|
||||
|
||||
static_assert(not is_subtype_of(TypeOf[a.f], Callable[[float], int]))
|
||||
static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int]))
|
||||
|
||||
# TODO: This assertion should be true
|
||||
# error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int]))
|
||||
static_assert(is_subtype_of(CallableTypeFromFunction[mixed], CallableTypeFromFunction[empty]))
|
||||
static_assert(not is_subtype_of(CallableTypeFromFunction[empty], CallableTypeFromFunction[mixed]))
|
||||
```
|
||||
|
||||
[special case for float and complex]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
|
||||
@@ -120,26 +120,3 @@ static_assert(is_subtype_of(typing.TypeAliasType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy))
|
||||
```
|
||||
|
||||
### `Callable` types always have ambiguous truthiness
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: Callable, y: Callable[[int], str]):
|
||||
reveal_type(bool(x)) # revealed: bool
|
||||
reveal_type(bool(y)) # revealed: bool
|
||||
```
|
||||
|
||||
But certain callable single-valued types are known to be always truthy:
|
||||
|
||||
```py
|
||||
from types import FunctionType
|
||||
|
||||
class A:
|
||||
def method(self): ...
|
||||
|
||||
reveal_type(bool(A().method)) # revealed: Literal[True]
|
||||
reveal_type(bool(f.__get__)) # revealed: Literal[True]
|
||||
reveal_type(bool(FunctionType.__get__)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -19,11 +19,6 @@ static_assert(is_equivalent_to(Never, tuple[int, Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
|
||||
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
|
||||
|
||||
def _(x: tuple[Never], y: tuple[int, Never], z: tuple[Never, int]):
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(y) # revealed: Never
|
||||
reveal_type(z) # revealed: Never
|
||||
```
|
||||
|
||||
The empty `tuple` is *not* equivalent to `Never`!
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
# Unreachable code
|
||||
|
||||
## Detecting unreachable code
|
||||
|
||||
In this section, we look at various scenarios how sections of code can become unreachable. We should
|
||||
eventually introduce a new diagnostic that would detect unreachable code.
|
||||
|
||||
### Terminal statements
|
||||
|
||||
In the following examples, the `print` statements are definitely unreachable.
|
||||
|
||||
```py
|
||||
def f1():
|
||||
return
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f2():
|
||||
raise Exception()
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f3():
|
||||
while True:
|
||||
break
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f4():
|
||||
for _ in range(10):
|
||||
continue
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
### Infinite loops
|
||||
|
||||
```py
|
||||
def f1():
|
||||
while True:
|
||||
pass
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
### Statically known branches
|
||||
|
||||
In the following examples, the `print` statements are also unreachable, but it requires type
|
||||
inference to determine that:
|
||||
|
||||
```py
|
||||
def f1():
|
||||
if 2 + 3 > 10:
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
|
||||
def f2():
|
||||
if True:
|
||||
return
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
### `Never` / `NoReturn`
|
||||
|
||||
If a function is annotated with a return type of `Never` or `NoReturn`, we can consider all code
|
||||
after the call to that function unreachable.
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn
|
||||
|
||||
def always_raises() -> NoReturn:
|
||||
raise Exception()
|
||||
|
||||
def f():
|
||||
always_raises()
|
||||
|
||||
# TODO: we should mark this as unreachable
|
||||
print("unreachable")
|
||||
```
|
||||
|
||||
## Python version and platform checks
|
||||
|
||||
It is common to have code that is specific to a certain Python version or platform. This case is
|
||||
special because whether or not the code is reachable depends on externally configured constants. And
|
||||
if we are checking for a set of parameters that makes one of these branches unreachable, that is
|
||||
likely not something that the user wants to be warned about, because there are probably other sets
|
||||
of parameters that make the branch reachable.
|
||||
|
||||
### `sys.version_info` branches
|
||||
|
||||
Consider the following example. If we check with a Python version lower than 3.11, the import
|
||||
statement is unreachable. If we check with a Python version equal to or greater than 3.11, the
|
||||
import statement is definitely reachable. We should not emit any diagnostics in either case.
|
||||
|
||||
#### Checking with Python version 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
# TODO: we should not emit an error here
|
||||
# error: [unresolved-import]
|
||||
from typing import Self
|
||||
```
|
||||
|
||||
#### Checking with Python version 3.12
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self
|
||||
```
|
||||
|
||||
### `sys.platform` branches
|
||||
|
||||
The problem is even more pronounced with `sys.platform` branches, since we don't necessarily have
|
||||
the platform information available.
|
||||
|
||||
#### Checking with platform `win32`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "win32"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
#### Checking with platform `linux`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "linux"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
# TODO: we should not emit an error here
|
||||
# error: [unresolved-attribute]
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
#### Checking without a specified platform
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# python-platform not specified
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
# TODO: we should not emit an error here
|
||||
# error: [possibly-unbound-attribute]
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
#### Checking with platform set to `all`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-platform = "all"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
# TODO: we should not emit an error here
|
||||
# error: [possibly-unbound-attribute]
|
||||
sys.getwindowsversion()
|
||||
```
|
||||
|
||||
## No false positive diagnostics in unreachable code
|
||||
|
||||
In this section, we make sure that we do not emit false positive diagnostics in unreachable code.
|
||||
|
||||
### Use of variables in unreachable code
|
||||
|
||||
We should not emit any diagnostics for uses of symbols in unreachable code:
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
return
|
||||
|
||||
print("unreachable")
|
||||
|
||||
print(x)
|
||||
```
|
||||
|
||||
### Use of variable in nested function
|
||||
|
||||
In the example below, since we use `x` in the `inner` function, we use the "public" type of `x`,
|
||||
which currently refers to the end-of-scope type of `x`. Since the end of the `outer` scope is
|
||||
unreachable, we treat `x` as if it was not defined. This behavior can certainly be improved.
|
||||
|
||||
```py
|
||||
def outer():
|
||||
x = 1
|
||||
|
||||
def inner():
|
||||
return x # Name `x` used when not defined
|
||||
while True:
|
||||
pass
|
||||
```
|
||||
|
||||
## No diagnostics in unreachable code
|
||||
|
||||
In general, no diagnostics should be emitted in unreachable code. The reasoning is that any issues
|
||||
inside the unreachable section would not cause problems at runtime. And type checking the
|
||||
unreachable code under the assumption that it *is* reachable might lead to false positives:
|
||||
|
||||
```py
|
||||
FEATURE_X_ACTIVATED = False
|
||||
|
||||
if FEATURE_X_ACTIVATED:
|
||||
def feature_x():
|
||||
print("Performing 'X'")
|
||||
|
||||
def f():
|
||||
if FEATURE_X_ACTIVATED:
|
||||
# Type checking this particular section as if it were reachable would
|
||||
# lead to a false positive, so we should not emit diagnostics here.
|
||||
|
||||
# TODO: no error should be emitted here
|
||||
# error: [unresolved-reference]
|
||||
feature_x()
|
||||
```
|
||||
@@ -19,14 +19,14 @@ pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::{default_lint_registry, ProgramSettings, PythonPath, PythonPlatform};
|
||||
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
|
||||
|
||||
use super::Db;
|
||||
use crate::lint::{LintRegistry, RuleSelection};
|
||||
use anyhow::Context;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{
|
||||
DbWithTestSystem, DbWithWritableSystem as _, System, SystemPath, SystemPathBuf, TestSystem,
|
||||
DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem,
|
||||
};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
@@ -139,8 +139,8 @@ pub(crate) mod tests {
|
||||
python_version: PythonVersion,
|
||||
/// Target Python platform
|
||||
python_platform: PythonPlatform,
|
||||
/// Paths to the directory to use for `site-packages`
|
||||
site_packages: Vec<SystemPathBuf>,
|
||||
/// 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)>,
|
||||
}
|
||||
@@ -150,7 +150,7 @@ pub(crate) mod tests {
|
||||
Self {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
site_packages: vec![],
|
||||
custom_typeshed: None,
|
||||
files: vec![],
|
||||
}
|
||||
}
|
||||
@@ -160,20 +160,8 @@ pub(crate) mod tests {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_file(
|
||||
mut self,
|
||||
path: &'a (impl AsRef<SystemPath> + ?Sized),
|
||||
content: &'a str,
|
||||
) -> Self {
|
||||
self.files.push((path.as_ref().as_str(), content));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_site_packages_search_path(
|
||||
mut self,
|
||||
path: &(impl AsRef<SystemPath> + ?Sized),
|
||||
) -> Self {
|
||||
self.site_packages.push(path.as_ref().to_path_buf());
|
||||
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
|
||||
self.files.push((path, content));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -187,7 +175,7 @@ pub(crate) mod tests {
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(vec![src_root]);
|
||||
search_paths.python_path = PythonPath::KnownSitePackages(self.site_packages);
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
|
||||
@@ -104,7 +104,6 @@ impl ModuleKind {
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum KnownModule {
|
||||
Builtins,
|
||||
Enum,
|
||||
Types,
|
||||
#[strum(serialize = "_typeshed")]
|
||||
Typeshed,
|
||||
@@ -122,7 +121,6 @@ impl KnownModule {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Enum => "enum",
|
||||
Self::Types => "types",
|
||||
Self::Typing => "typing",
|
||||
Self::Typeshed => "_typeshed",
|
||||
@@ -166,10 +164,6 @@ impl KnownModule {
|
||||
pub const fn is_inspect(self) -> bool {
|
||||
matches!(self, Self::Inspect)
|
||||
}
|
||||
|
||||
pub const fn is_enum(self) -> bool {
|
||||
matches!(self, Self::Enum)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KnownModule {
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_python_ast::PythonVersion;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
|
||||
use crate::site_packages::{SitePackagesDiscoveryError, SysPrefixPathOrigin, VirtualEnvironment};
|
||||
use crate::site_packages::VirtualEnvironment;
|
||||
use crate::{Program, PythonPath, SearchPathSettings};
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
@@ -96,13 +96,18 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
FilePath::SystemVirtual(_) => return None,
|
||||
};
|
||||
|
||||
let module_name = search_paths(db).find_map(|candidate| {
|
||||
let mut search_paths = search_paths(db);
|
||||
|
||||
let module_name = loop {
|
||||
let candidate = search_paths.next()?;
|
||||
let relative_path = match path {
|
||||
SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path),
|
||||
SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path),
|
||||
}?;
|
||||
relative_path.to_module_name()
|
||||
})?;
|
||||
};
|
||||
if let Some(relative_path) = relative_path {
|
||||
break relative_path.to_module_name()?;
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the module name to see if Python would resolve the name to the same path.
|
||||
// If it doesn't, then that means that multiple modules have the same name in different
|
||||
@@ -110,7 +115,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
// in which case we ignore it.
|
||||
let module = resolve_module(db, &module_name)?;
|
||||
|
||||
if file.path(db) == module.file().path(db) {
|
||||
if file == module.file() {
|
||||
Some(module)
|
||||
} else {
|
||||
// This path is for a module with the same name but with a different precedence. For example:
|
||||
@@ -128,15 +133,6 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
|
||||
Program::get(db).search_paths(db).iter(db)
|
||||
}
|
||||
|
||||
/// Searches for a `.venv` directory in `project_root` that contains a `pyvenv.cfg` file.
|
||||
fn discover_venv_in(system: &dyn System, project_root: &SystemPath) -> Option<SystemPathBuf> {
|
||||
let virtual_env_directory = project_root.join(".venv");
|
||||
|
||||
system
|
||||
.is_file(&virtual_env_directory.join("pyvenv.cfg"))
|
||||
.then_some(virtual_env_directory)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SearchPaths {
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
@@ -239,37 +235,6 @@ impl SearchPaths {
|
||||
.and_then(|venv| venv.site_packages_directories(system))?
|
||||
}
|
||||
|
||||
PythonPath::Discover(root) => {
|
||||
tracing::debug!("Discovering virtual environment in `{root}`");
|
||||
let virtual_env_path = discover_venv_in(db.system(), root);
|
||||
if let Some(virtual_env_path) = virtual_env_path {
|
||||
tracing::debug!("Found `.venv` folder at `{}`", virtual_env_path);
|
||||
|
||||
let handle_invalid_virtual_env = |error: SitePackagesDiscoveryError| {
|
||||
tracing::debug!(
|
||||
"Ignoring automatically detected virtual environment at `{}`: {}",
|
||||
virtual_env_path,
|
||||
error
|
||||
);
|
||||
vec![]
|
||||
};
|
||||
|
||||
match VirtualEnvironment::new(
|
||||
virtual_env_path.clone(),
|
||||
SysPrefixPathOrigin::LocalVenv,
|
||||
system,
|
||||
) {
|
||||
Ok(venv) => venv
|
||||
.site_packages_directories(system)
|
||||
.unwrap_or_else(handle_invalid_virtual_env),
|
||||
Err(error) => handle_invalid_virtual_env(error),
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("No virtual environment found");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
PythonPath::KnownSitePackages(paths) => paths
|
||||
.iter()
|
||||
.map(|path| canonicalize(path, system))
|
||||
@@ -1964,33 +1929,4 @@ not_a_directory
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_to_module_where_one_search_path_is_subdirectory_of_other() {
|
||||
let project_directory = SystemPathBuf::from("/project");
|
||||
let site_packages = project_directory.join(".venv/lib/python3.13/site-packages");
|
||||
let installed_foo_module = site_packages.join("foo/__init__.py");
|
||||
|
||||
let mut db = TestDb::new();
|
||||
db.write_file(&installed_foo_module, "").unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![project_directory],
|
||||
custom_typeshed: None,
|
||||
python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let foo_module_file = File::new(&db, FilePath::System(installed_foo_module));
|
||||
let module = file_to_module(&db, foo_module_file).unwrap();
|
||||
assert_eq!(module.search_path(), &site_packages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,6 @@ fn python_version_from_versions_file_string(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write as _;
|
||||
use std::num::{IntErrorKind, NonZeroU16};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -334,7 +333,8 @@ mod tests {
|
||||
|
||||
const TYPESHED_STDLIB_DIR: &str = "stdlib";
|
||||
|
||||
const ONE: Option<NonZeroU16> = Some(NonZeroU16::new(1).unwrap());
|
||||
#[allow(unsafe_code)]
|
||||
const ONE: Option<NonZeroU16> = Some(unsafe { NonZeroU16::new_unchecked(1) });
|
||||
|
||||
impl TypeshedVersions {
|
||||
#[must_use]
|
||||
@@ -571,7 +571,7 @@ foo: 3.8- # trailing comment
|
||||
|
||||
let mut massive_versions_file = String::new();
|
||||
for i in 0..too_many {
|
||||
let _ = writeln!(&mut massive_versions_file, "x{i}: 3.8-");
|
||||
massive_versions_file.push_str(&format!("x{i}: 3.8-\n"));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -145,9 +145,6 @@ pub enum PythonPath {
|
||||
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
|
||||
SysPrefix(SystemPathBuf, SysPrefixPathOrigin),
|
||||
|
||||
/// Tries to discover a virtual environment in the given path.
|
||||
Discover(SystemPathBuf),
|
||||
|
||||
/// Resolved site packages paths.
|
||||
///
|
||||
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`
|
||||
|
||||
@@ -124,12 +124,6 @@ pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
FileScopeId::global().to_scope_id(db, file)
|
||||
}
|
||||
|
||||
pub(crate) enum EagerBindingsResult<'map, 'db> {
|
||||
Found(BindingWithConstraintsIterator<'map, 'db>),
|
||||
NotFound,
|
||||
NoLongerInEagerContext,
|
||||
}
|
||||
|
||||
/// The symbol tables and use-def maps for all scopes in a file.
|
||||
#[derive(Debug, Update)]
|
||||
pub(crate) struct SemanticIndex<'db> {
|
||||
@@ -325,40 +319,21 @@ impl<'db> SemanticIndex<'db> {
|
||||
self.has_future_annotations
|
||||
}
|
||||
|
||||
/// Returns
|
||||
/// * `NoLongerInEagerContext` if the nested scope is no longer in an eager context
|
||||
/// (that is, not every scope that will be traversed is eager).
|
||||
/// * an iterator of bindings for a particular nested eager scope reference if the bindings exist.
|
||||
/// * `NotFound` if the bindings do not exist in the nested eager scope.
|
||||
/// Returns an iterator of bindings for a particular nested eager scope reference.
|
||||
pub(crate) fn eager_bindings(
|
||||
&self,
|
||||
enclosing_scope: FileScopeId,
|
||||
symbol: &str,
|
||||
nested_scope: FileScopeId,
|
||||
) -> EagerBindingsResult<'_, 'db> {
|
||||
for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) {
|
||||
if ancestor_scope_id == enclosing_scope {
|
||||
break;
|
||||
}
|
||||
if !ancestor_scope.is_eager() {
|
||||
return EagerBindingsResult::NoLongerInEagerContext;
|
||||
}
|
||||
}
|
||||
let Some(symbol_id) = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol) else {
|
||||
return EagerBindingsResult::NotFound;
|
||||
};
|
||||
) -> Option<BindingWithConstraintsIterator<'_, 'db>> {
|
||||
let symbol_id = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol)?;
|
||||
let key = EagerBindingsKey {
|
||||
enclosing_scope,
|
||||
enclosing_symbol: symbol_id,
|
||||
nested_scope,
|
||||
};
|
||||
let Some(id) = self.eager_bindings.get(&key) else {
|
||||
return EagerBindingsResult::NotFound;
|
||||
};
|
||||
match self.use_def_maps[enclosing_scope].eager_bindings(*id) {
|
||||
Some(bindings) => EagerBindingsResult::Found(bindings),
|
||||
None => EagerBindingsResult::NotFound,
|
||||
}
|
||||
let id = self.eager_bindings.get(&key)?;
|
||||
self.use_def_maps[enclosing_scope].eager_bindings(*id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::semantic_index::visibility_constraints::{
|
||||
ScopedVisibilityConstraintId, VisibilityConstraintsBuilder,
|
||||
};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue};
|
||||
use crate::unpack::{Unpack, UnpackValue};
|
||||
use crate::Db;
|
||||
|
||||
mod except_handlers;
|
||||
@@ -256,11 +256,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
|
||||
for nested_symbol in self.symbol_tables[popped_scope_id].symbols() {
|
||||
// Skip this symbol if this enclosing scope doesn't contain any bindings for it.
|
||||
// Note that even if this symbol is bound in the popped scope,
|
||||
// it may refer to the enclosing scope bindings
|
||||
// so we also need to snapshot the bindings of the enclosing scope.
|
||||
|
||||
// Skip this symbol if this enclosing scope doesn't contain any bindings for
|
||||
// it, or if the nested scope _does_.
|
||||
if nested_symbol.is_bound() {
|
||||
continue;
|
||||
}
|
||||
let Some(enclosing_symbol_id) =
|
||||
enclosing_symbol_table.symbol_id_by_name(nested_symbol.name())
|
||||
else {
|
||||
@@ -589,31 +589,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> {
|
||||
match pattern {
|
||||
ast::Pattern::MatchValue(pattern) => {
|
||||
let value = self.add_standalone_expression(&pattern.value);
|
||||
PatternPredicateKind::Value(value)
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton) => {
|
||||
PatternPredicateKind::Singleton(singleton.value)
|
||||
}
|
||||
ast::Pattern::MatchClass(pattern) => {
|
||||
let cls = self.add_standalone_expression(&pattern.cls);
|
||||
PatternPredicateKind::Class(cls)
|
||||
}
|
||||
ast::Pattern::MatchOr(pattern) => {
|
||||
let predicates = pattern
|
||||
.patterns
|
||||
.iter()
|
||||
.map(|pattern| self.predicate_kind(pattern))
|
||||
.collect();
|
||||
PatternPredicateKind::Or(predicates)
|
||||
}
|
||||
_ => PatternPredicateKind::Unsupported,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pattern_narrowing_constraint(
|
||||
&mut self,
|
||||
subject: Expression<'db>,
|
||||
@@ -631,16 +606,29 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
//
|
||||
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
|
||||
|
||||
let kind = self.predicate_kind(pattern);
|
||||
let guard = guard.map(|guard| self.add_standalone_expression(guard));
|
||||
|
||||
let kind = match pattern {
|
||||
ast::Pattern::MatchValue(pattern) => {
|
||||
let value = self.add_standalone_expression(&pattern.value);
|
||||
PatternPredicateKind::Value(value, guard)
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton) => {
|
||||
PatternPredicateKind::Singleton(singleton.value, guard)
|
||||
}
|
||||
ast::Pattern::MatchClass(pattern) => {
|
||||
let cls = self.add_standalone_expression(&pattern.cls);
|
||||
PatternPredicateKind::Class(cls, guard)
|
||||
}
|
||||
_ => PatternPredicateKind::Unsupported,
|
||||
};
|
||||
|
||||
let pattern_predicate = PatternPredicate::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
subject,
|
||||
kind,
|
||||
guard,
|
||||
countme::Count::default(),
|
||||
);
|
||||
let predicate = Predicate {
|
||||
@@ -831,64 +819,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
|
||||
/// Add an unpackable assignment for the given [`Unpackable`].
|
||||
///
|
||||
/// This method handles assignments that can contain unpacking like assignment statements,
|
||||
/// for statements, etc.
|
||||
fn add_unpackable_assignment(
|
||||
&mut self,
|
||||
unpackable: &Unpackable<'db>,
|
||||
target: &'db ast::Expr,
|
||||
value: Expression<'db>,
|
||||
) {
|
||||
// We only handle assignments to names and unpackings here, other targets like
|
||||
// attribute and subscript are handled separately as they don't create a new
|
||||
// definition.
|
||||
|
||||
let current_assignment = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
let unpack = Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
UnpackValue::new(unpackable.kind(), value),
|
||||
countme::Count::default(),
|
||||
));
|
||||
Some(unpackable.as_current_assignment(unpack))
|
||||
}
|
||||
ast::Expr::Name(_) => Some(unpackable.as_current_assignment(None)),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
unpackable.as_attribute_assignment(value),
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
|
||||
self.visit_expr(target);
|
||||
|
||||
if current_assignment.is_some() {
|
||||
// Only need to pop in the case where we pushed something
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
||||
let module = self.module;
|
||||
self.visit_body(module.suite());
|
||||
@@ -1188,7 +1118,59 @@ where
|
||||
let value = self.add_standalone_expression(&node.value);
|
||||
|
||||
for target in &node.targets {
|
||||
self.add_unpackable_assignment(&Unpackable::Assign(node), target, value);
|
||||
// We only handle assignments to names and unpackings here, other targets like
|
||||
// attribute and subscript are handled separately as they don't create a new
|
||||
// definition.
|
||||
let current_assignment = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
UnpackValue::Assign(value),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Unannotated { value },
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
|
||||
self.visit_expr(target);
|
||||
|
||||
if current_assignment.is_some() {
|
||||
// Only need to pop in the case where we pushed something
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Stmt::AnnAssign(node) => {
|
||||
@@ -1379,7 +1361,7 @@ where
|
||||
is_async,
|
||||
..
|
||||
}) => {
|
||||
for item @ ast::WithItem {
|
||||
for item @ ruff_python_ast::WithItem {
|
||||
range: _,
|
||||
context_expr,
|
||||
optional_vars,
|
||||
@@ -1388,14 +1370,55 @@ where
|
||||
self.visit_expr(context_expr);
|
||||
if let Some(optional_vars) = optional_vars.as_deref() {
|
||||
let context_manager = self.add_standalone_expression(context_expr);
|
||||
self.add_unpackable_assignment(
|
||||
&Unpackable::WithItem {
|
||||
let current_assignment = match optional_vars {
|
||||
ast::Expr::Tuple(_) | ast::Expr::List(_) => {
|
||||
Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
first: true,
|
||||
is_async: *is_async,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: the node `optional_vars` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), optional_vars)
|
||||
},
|
||||
UnpackValue::ContextManager(context_manager),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
},
|
||||
optional_vars,
|
||||
context_manager,
|
||||
);
|
||||
unpack: None,
|
||||
// `false` is arbitrary here---we don't actually use it other than in the actual unpacks
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::ContextManager { context_manager },
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
self.visit_expr(optional_vars);
|
||||
if current_assignment.is_some() {
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.visit_body(body);
|
||||
@@ -1420,7 +1443,52 @@ where
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
|
||||
self.add_unpackable_assignment(&Unpackable::For(for_stmt), target, iter_expr);
|
||||
let current_assignment = match &**target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(CurrentAssignment::For {
|
||||
node: for_stmt,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
// SAFETY: the node `target` belongs to the `self.module` tree
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
UnpackValue::Iterable(iter_expr),
|
||||
countme::Count::default(),
|
||||
)),
|
||||
}),
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::For {
|
||||
node: for_stmt,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Iterable {
|
||||
iterable: iter_expr,
|
||||
},
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
self.visit_expr(target);
|
||||
if current_assignment.is_some() {
|
||||
self.pop_assignment();
|
||||
}
|
||||
|
||||
let outer_loop = self.push_loop();
|
||||
self.visit_body(body);
|
||||
@@ -1448,7 +1516,7 @@ where
|
||||
self.visit_expr(subject);
|
||||
if cases.is_empty() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let mut vis_constraints = vec![];
|
||||
@@ -1657,13 +1725,18 @@ where
|
||||
|
||||
if is_definition {
|
||||
match self.current_assignment() {
|
||||
Some(CurrentAssignment::Assign { node, unpack }) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
AssignmentDefinitionNodeRef {
|
||||
unpack,
|
||||
value: &node.value,
|
||||
name: name_node,
|
||||
first,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1673,11 +1746,16 @@ where
|
||||
Some(CurrentAssignment::AugAssign(aug_assign)) => {
|
||||
self.add_definition(symbol, aug_assign);
|
||||
}
|
||||
Some(CurrentAssignment::For { node, unpack }) => {
|
||||
Some(CurrentAssignment::For {
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ForStmtDefinitionNodeRef {
|
||||
unpack,
|
||||
first,
|
||||
iterable: &node.iter,
|
||||
name: name_node,
|
||||
is_async: node.is_async,
|
||||
@@ -1703,6 +1781,7 @@ where
|
||||
}
|
||||
Some(CurrentAssignment::WithItem {
|
||||
item,
|
||||
first,
|
||||
is_async,
|
||||
unpack,
|
||||
}) => {
|
||||
@@ -1712,6 +1791,7 @@ where
|
||||
unpack,
|
||||
context_expr: &item.context_expr,
|
||||
name: name_node,
|
||||
first,
|
||||
is_async,
|
||||
},
|
||||
);
|
||||
@@ -1720,11 +1800,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(unpack_position) = self
|
||||
.current_assignment_mut()
|
||||
.and_then(CurrentAssignment::unpack_position_mut)
|
||||
if let Some(
|
||||
CurrentAssignment::Assign { first, .. }
|
||||
| CurrentAssignment::For { first, .. }
|
||||
| CurrentAssignment::WithItem { first, .. },
|
||||
) = self.current_assignment_mut()
|
||||
{
|
||||
*unpack_position = UnpackPosition::Other;
|
||||
*first = false;
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
@@ -1893,10 +1975,20 @@ where
|
||||
ctx: ExprContext::Store,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(unpack) = self
|
||||
.current_assignment()
|
||||
.as_ref()
|
||||
.and_then(CurrentAssignment::unpack)
|
||||
if let Some(
|
||||
CurrentAssignment::Assign {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
}
|
||||
| CurrentAssignment::For {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
}
|
||||
| CurrentAssignment::WithItem {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
},
|
||||
) = self.current_assignment()
|
||||
{
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
@@ -1971,13 +2063,15 @@ where
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign {
|
||||
node: &'a ast::StmtAssign,
|
||||
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
For {
|
||||
node: &'a ast::StmtFor,
|
||||
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
Named(&'a ast::ExprNamed),
|
||||
Comprehension {
|
||||
@@ -1986,37 +2080,12 @@ enum CurrentAssignment<'a> {
|
||||
},
|
||||
WithItem {
|
||||
item: &'a ast::WithItem,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> CurrentAssignment<'a> {
|
||||
fn unpack(&self) -> Option<Unpack<'a>> {
|
||||
match self {
|
||||
Self::Assign { unpack, .. }
|
||||
| Self::For { unpack, .. }
|
||||
| Self::WithItem { unpack, .. } => unpack.map(|(_, unpack)| unpack),
|
||||
Self::AnnAssign(_)
|
||||
| Self::AugAssign(_)
|
||||
| Self::Named(_)
|
||||
| Self::Comprehension { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_position_mut(&mut self) -> Option<&mut UnpackPosition> {
|
||||
match self {
|
||||
Self::Assign { unpack, .. }
|
||||
| Self::For { unpack, .. }
|
||||
| Self::WithItem { unpack, .. } => unpack.as_mut().map(|(position, _)| position),
|
||||
Self::AnnAssign(_)
|
||||
| Self::AugAssign(_)
|
||||
| Self::Named(_)
|
||||
| Self::Comprehension { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtAnnAssign) -> Self {
|
||||
Self::AnnAssign(value)
|
||||
@@ -2059,47 +2128,3 @@ impl<'a> CurrentMatchCase<'a> {
|
||||
Self { pattern, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
enum Unpackable<'a> {
|
||||
Assign(&'a ast::StmtAssign),
|
||||
For(&'a ast::StmtFor),
|
||||
WithItem {
|
||||
item: &'a ast::WithItem,
|
||||
is_async: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Unpackable<'a> {
|
||||
const fn kind(&self) -> UnpackKind {
|
||||
match self {
|
||||
Unpackable::Assign(_) => UnpackKind::Assign,
|
||||
Unpackable::For(_) => UnpackKind::Iterable,
|
||||
Unpackable::WithItem { .. } => UnpackKind::ContextManager,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_current_assignment(&self, unpack: Option<Unpack<'a>>) -> CurrentAssignment<'a> {
|
||||
let unpack = unpack.map(|unpack| (UnpackPosition::First, unpack));
|
||||
match self {
|
||||
Unpackable::Assign(stmt) => CurrentAssignment::Assign { node: stmt, unpack },
|
||||
Unpackable::For(stmt) => CurrentAssignment::For { node: stmt, unpack },
|
||||
Unpackable::WithItem { item, is_async } => CurrentAssignment::WithItem {
|
||||
item,
|
||||
is_async: *is_async,
|
||||
unpack,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn as_attribute_assignment(&self, expression: Expression<'a>) -> AttributeAssignment<'a> {
|
||||
match self {
|
||||
Unpackable::Assign(_) => AttributeAssignment::Unannotated { value: expression },
|
||||
Unpackable::For(_) => AttributeAssignment::Iterable {
|
||||
iterable: expression,
|
||||
},
|
||||
Unpackable::WithItem { .. } => AttributeAssignment::ContextManager {
|
||||
context_manager: expression,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -8,18 +8,22 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
|
||||
/// A definition of a symbol.
|
||||
///
|
||||
/// ## ID stability
|
||||
/// The `Definition`'s ID is stable when the only field that change is its `kind` (AST node).
|
||||
/// ## Module-local type
|
||||
/// This type should not be used as part of any cross-module API because
|
||||
/// it holds a reference to the AST node. Range-offset changes
|
||||
/// then propagate through all usages, and deserialization requires
|
||||
/// reparsing the entire module.
|
||||
///
|
||||
/// The `Definition` changes when the `file`, `scope`, or `symbol` change. This can be
|
||||
/// because a new scope gets inserted before the `Definition` or a new symbol is inserted
|
||||
/// before this `Definition`. However, the ID can be considered stable and it is okay to use
|
||||
/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs.
|
||||
/// E.g. don't use this type in:
|
||||
///
|
||||
/// * a return type of a cross-module query
|
||||
/// * a field of a type that is a return type of a cross-module query
|
||||
/// * an argument of a cross-module query
|
||||
#[salsa::tracked(debug)]
|
||||
pub struct Definition<'db> {
|
||||
/// The file in which the definition occurs.
|
||||
@@ -48,14 +52,6 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
|
||||
pub fn full_range(self, db: &'db dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.kind(db).full_range())
|
||||
}
|
||||
|
||||
pub fn focus_range(self, db: &'db dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.kind(db).target_range())
|
||||
}
|
||||
}
|
||||
|
||||
/// One or more [`Definition`]s.
|
||||
@@ -243,24 +239,27 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) value: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) context_expr: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
@@ -333,10 +332,12 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
value,
|
||||
name,
|
||||
first,
|
||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
value: AstNodeRef::new(parsed.clone(), value),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
}),
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
@@ -348,11 +349,13 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
iterable,
|
||||
name,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::For(ForStmtDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
||||
@@ -379,11 +382,13 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
context_expr,
|
||||
name,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
context_expr: AstNodeRef::new(parsed.clone(), context_expr),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
||||
@@ -446,6 +451,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
value: _,
|
||||
unpack: _,
|
||||
name,
|
||||
first: _,
|
||||
}) => name.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
@@ -453,6 +459,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack: _,
|
||||
iterable: _,
|
||||
name,
|
||||
first: _,
|
||||
is_async: _,
|
||||
}) => name.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
||||
@@ -462,6 +469,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
Self::WithItem(WithItemDefinitionNodeRef {
|
||||
unpack: _,
|
||||
context_expr: _,
|
||||
first: _,
|
||||
is_async: _,
|
||||
name,
|
||||
}) => name.into(),
|
||||
@@ -563,6 +571,8 @@ impl DefinitionKind<'_> {
|
||||
///
|
||||
/// A definition target would mainly be the node representing the symbol being defined i.e.,
|
||||
/// [`ast::ExprName`] or [`ast::Identifier`] but could also be other nodes.
|
||||
///
|
||||
/// This is mainly used for logging and debugging purposes.
|
||||
pub(crate) fn target_range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.alias().range(),
|
||||
@@ -589,33 +599,6 @@ impl DefinitionKind<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TextRange`] of the entire definition.
|
||||
pub(crate) fn full_range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.alias().range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
DefinitionKind::StarImport(import) => import.import().range(),
|
||||
DefinitionKind::Function(function) => function.range(),
|
||||
DefinitionKind::Class(class) => class.range(),
|
||||
DefinitionKind::TypeAlias(type_alias) => type_alias.range(),
|
||||
DefinitionKind::NamedExpression(named) => named.range(),
|
||||
DefinitionKind::Assignment(assignment) => assignment.name().range(),
|
||||
DefinitionKind::AnnotatedAssignment(assign) => assign.range(),
|
||||
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
|
||||
DefinitionKind::Comprehension(comp) => comp.target().range(),
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.range(),
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.range(),
|
||||
DefinitionKind::Parameter(parameter) => parameter.parameter.range(),
|
||||
DefinitionKind::WithItem(with_item) => with_item.name().range(),
|
||||
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
|
||||
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
|
||||
DefinitionKind::TypeVar(type_var) => type_var.range(),
|
||||
DefinitionKind::ParamSpec(param_spec) => param_spec.range(),
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.range(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn category(&self, in_stub: bool) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
@@ -669,14 +652,14 @@ impl DefinitionKind<'_> {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
|
||||
pub(crate) enum TargetKind<'db> {
|
||||
Sequence(UnpackPosition, Unpack<'db>),
|
||||
Sequence(Unpack<'db>),
|
||||
Name,
|
||||
}
|
||||
|
||||
impl<'db> From<Option<(UnpackPosition, Unpack<'db>)>> for TargetKind<'db> {
|
||||
fn from(value: Option<(UnpackPosition, Unpack<'db>)>) -> Self {
|
||||
impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
|
||||
fn from(value: Option<Unpack<'db>>) -> Self {
|
||||
match value {
|
||||
Some((unpack_position, unpack)) => TargetKind::Sequence(unpack_position, unpack),
|
||||
Some(unpack) => TargetKind::Sequence(unpack),
|
||||
None => TargetKind::Name,
|
||||
}
|
||||
}
|
||||
@@ -797,6 +780,7 @@ pub struct AssignmentDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
value: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl<'db> AssignmentDefinitionKind<'db> {
|
||||
@@ -811,6 +795,10 @@ impl<'db> AssignmentDefinitionKind<'db> {
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -818,6 +806,7 @@ pub struct WithItemDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
context_expr: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
@@ -834,6 +823,10 @@ impl<'db> WithItemDefinitionKind<'db> {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
@@ -844,6 +837,7 @@ pub struct ForStmtDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
@@ -860,6 +854,10 @@ impl<'db> ForStmtDefinitionKind<'db> {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) const fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
|
||||
@@ -57,10 +57,9 @@ pub(crate) enum PredicateNode<'db> {
|
||||
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
|
||||
pub(crate) enum PatternPredicateKind<'db> {
|
||||
Singleton(Singleton),
|
||||
Value(Expression<'db>),
|
||||
Or(Vec<PatternPredicateKind<'db>>),
|
||||
Class(Expression<'db>),
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
Class(Expression<'db>, Option<Expression<'db>>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
@@ -75,8 +74,6 @@ pub(crate) struct PatternPredicate<'db> {
|
||||
#[return_ref]
|
||||
pub(crate) kind: PatternPredicateKind<'db>,
|
||||
|
||||
pub(crate) guard: Option<Expression<'db>>,
|
||||
|
||||
count: countme::Count<PatternPredicate<'static>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -114,10 +114,6 @@ impl<'db> ScopeId<'db> {
|
||||
self.node(db).scope_kind().is_function_like()
|
||||
}
|
||||
|
||||
pub(crate) fn is_type_parameter(self, db: &'db dyn Db) -> bool {
|
||||
self.node(db).scope_kind().is_type_parameter()
|
||||
}
|
||||
|
||||
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
|
||||
self.scope(db).node()
|
||||
}
|
||||
@@ -230,8 +226,9 @@ pub enum ScopeKind {
|
||||
impl ScopeKind {
|
||||
pub(crate) fn is_eager(self) -> bool {
|
||||
match self {
|
||||
ScopeKind::Module | ScopeKind::Class | ScopeKind::Comprehension => true,
|
||||
ScopeKind::Annotation
|
||||
ScopeKind::Class | ScopeKind::Comprehension => true,
|
||||
ScopeKind::Module
|
||||
| ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias => false,
|
||||
@@ -254,14 +251,10 @@ impl ScopeKind {
|
||||
pub(crate) fn is_class(self) -> bool {
|
||||
matches!(self, ScopeKind::Class)
|
||||
}
|
||||
|
||||
pub(crate) fn is_type_parameter(self) -> bool {
|
||||
matches!(self, ScopeKind::Annotation | ScopeKind::TypeAlias)
|
||||
}
|
||||
}
|
||||
|
||||
/// Symbol table for a specific [`Scope`].
|
||||
#[derive(Default, salsa::Update)]
|
||||
#[derive(Debug, Default, salsa::Update)]
|
||||
pub struct SymbolTable {
|
||||
/// The symbols in this scope.
|
||||
symbols: IndexVec<ScopedSymbolId, Symbol>,
|
||||
@@ -322,16 +315,6 @@ impl PartialEq for SymbolTable {
|
||||
|
||||
impl Eq for SymbolTable {}
|
||||
|
||||
impl std::fmt::Debug for SymbolTable {
|
||||
/// Exclude the `symbols_by_name` field from the debug output.
|
||||
/// It's very noisy and not useful for debugging.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("SymbolTable")
|
||||
.field(&self.symbols)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct SymbolTableBuilder {
|
||||
table: SymbolTable,
|
||||
|
||||
@@ -178,11 +178,10 @@ use std::cmp::Ordering;
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::predicate::{
|
||||
PatternPredicate, PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
|
||||
PatternPredicateKind, Predicate, PredicateNode, Predicates, ScopedPredicateId,
|
||||
};
|
||||
use crate::types::{infer_expression_type, Truthiness, Type};
|
||||
use crate::types::{infer_expression_type, Truthiness};
|
||||
use crate::Db;
|
||||
|
||||
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
|
||||
@@ -554,102 +553,36 @@ impl VisibilityConstraints {
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single_pattern_predicate_kind<'db>(
|
||||
db: &'db dyn Db,
|
||||
predicate_kind: &PatternPredicateKind<'db>,
|
||||
subject: Expression<'db>,
|
||||
) -> Truthiness {
|
||||
match predicate_kind {
|
||||
PatternPredicateKind::Value(value) => {
|
||||
let subject_ty = infer_expression_type(db, subject);
|
||||
let value_ty = infer_expression_type(db, *value);
|
||||
|
||||
if subject_ty.is_single_valued(db) {
|
||||
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty))
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternPredicateKind::Singleton(singleton) => {
|
||||
let subject_ty = infer_expression_type(db, subject);
|
||||
|
||||
let singleton_ty = match singleton {
|
||||
ruff_python_ast::Singleton::None => Type::none(db),
|
||||
ruff_python_ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ruff_python_ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
|
||||
debug_assert!(singleton_ty.is_singleton(db));
|
||||
|
||||
if subject_ty.is_equivalent_to(db, singleton_ty) {
|
||||
Truthiness::AlwaysTrue
|
||||
} else if subject_ty.is_disjoint_from(db, singleton_ty) {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternPredicateKind::Or(predicates) => {
|
||||
use std::ops::ControlFlow;
|
||||
let (ControlFlow::Break(truthiness) | ControlFlow::Continue(truthiness)) =
|
||||
predicates
|
||||
.iter()
|
||||
.map(|p| Self::analyze_single_pattern_predicate_kind(db, p, subject))
|
||||
// this is just a "max", but with a slight optimization: `AlwaysTrue` is the "greatest" possible element, so we short-circuit if we get there
|
||||
.try_fold(Truthiness::AlwaysFalse, |acc, next| match (acc, next) {
|
||||
(Truthiness::AlwaysTrue, _) | (_, Truthiness::AlwaysTrue) => {
|
||||
ControlFlow::Break(Truthiness::AlwaysTrue)
|
||||
}
|
||||
(Truthiness::Ambiguous, _) | (_, Truthiness::Ambiguous) => {
|
||||
ControlFlow::Continue(Truthiness::Ambiguous)
|
||||
}
|
||||
(Truthiness::AlwaysFalse, Truthiness::AlwaysFalse) => {
|
||||
ControlFlow::Continue(Truthiness::AlwaysFalse)
|
||||
}
|
||||
});
|
||||
truthiness
|
||||
}
|
||||
PatternPredicateKind::Class(class_expr) => {
|
||||
let subject_ty = infer_expression_type(db, subject);
|
||||
let class_ty = infer_expression_type(db, *class_expr).to_instance(db);
|
||||
|
||||
class_ty.map_or(Truthiness::Ambiguous, |class_ty| {
|
||||
if subject_ty.is_subtype_of(db, class_ty) {
|
||||
Truthiness::AlwaysTrue
|
||||
} else if subject_ty.is_disjoint_from(db, class_ty) {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
})
|
||||
}
|
||||
PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness {
|
||||
let truthiness = Self::analyze_single_pattern_predicate_kind(
|
||||
db,
|
||||
predicate.kind(db),
|
||||
predicate.subject(db),
|
||||
);
|
||||
|
||||
if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() {
|
||||
// Fall back to ambiguous, the guard might change the result.
|
||||
// TODO: actually analyze guard truthiness
|
||||
Truthiness::Ambiguous
|
||||
} else {
|
||||
truthiness
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
|
||||
match predicate.node {
|
||||
PredicateNode::Expression(test_expr) => {
|
||||
let ty = infer_expression_type(db, test_expr);
|
||||
ty.bool(db).negate_if(!predicate.is_positive)
|
||||
}
|
||||
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
|
||||
PredicateNode::Pattern(inner) => match inner.kind(db) {
|
||||
PatternPredicateKind::Value(value, guard) => {
|
||||
let subject_expression = inner.subject(db);
|
||||
let subject_ty = infer_expression_type(db, subject_expression);
|
||||
let value_ty = infer_expression_type(db, *value);
|
||||
|
||||
if subject_ty.is_single_valued(db) {
|
||||
let truthiness =
|
||||
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty));
|
||||
|
||||
if truthiness.is_always_true() && guard.is_some() {
|
||||
// Fall back to ambiguous, the guard might change the result.
|
||||
Truthiness::Ambiguous
|
||||
} else {
|
||||
truthiness
|
||||
}
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
PatternPredicateKind::Singleton(..)
|
||||
| PatternPredicateKind::Class(..)
|
||||
| PatternPredicateKind::Unsupported => Truthiness::Ambiguous,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ impl_binding_has_ty!(ast::StmtFunctionDef);
|
||||
impl_binding_has_ty!(ast::StmtClassDef);
|
||||
impl_binding_has_ty!(ast::Parameter);
|
||||
impl_binding_has_ty!(ast::ParameterWithDefault);
|
||||
impl_binding_has_ty!(ast::ExceptHandlerExceptHandler);
|
||||
|
||||
impl HasType for ast::Alias {
|
||||
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
|
||||
@@ -459,7 +459,6 @@ pub enum SysPrefixPathOrigin {
|
||||
PythonCliFlag,
|
||||
VirtualEnvVar,
|
||||
Derived,
|
||||
LocalVenv,
|
||||
}
|
||||
|
||||
impl Display for SysPrefixPathOrigin {
|
||||
@@ -468,7 +467,6 @@ impl Display for SysPrefixPathOrigin {
|
||||
Self::PythonCliFlag => f.write_str("`--python` argument"),
|
||||
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
|
||||
Self::Derived => f.write_str("derived `sys.prefix` path"),
|
||||
Self::LocalVenv => f.write_str("local virtual environment"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus};
|
||||
use crate::types::TypeCheckDiagnostics;
|
||||
use crate::types::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
use crate::{declare_lint, lint::LintId, Db};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Span};
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_db::{files::File, parsed::parsed_module, source::source_text};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_python_trivia::Cursor;
|
||||
@@ -145,7 +145,7 @@ pub(crate) fn check_suppressions(db: &dyn Db, file: File, diagnostics: &mut Type
|
||||
fn check_unknown_rule(context: &mut CheckSuppressionsContext) {
|
||||
if context.is_lint_disabled(&UNKNOWN_RULE) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for unknown in &context.suppressions.unknown {
|
||||
match &unknown.reason {
|
||||
@@ -174,7 +174,7 @@ fn check_unknown_rule(context: &mut CheckSuppressionsContext) {
|
||||
format_args!("Unknown rule `{prefixed}`. Did you mean `{suggestion}`?"),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
||||
suppression.range,
|
||||
format_args!("Unused `{kind}` without a code", kind = suppression.kind),
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,11 +319,14 @@ impl<'a> CheckSuppressionsContext<'a> {
|
||||
return;
|
||||
};
|
||||
|
||||
let id = DiagnosticId::Lint(lint.name());
|
||||
let mut diag = Diagnostic::new(id, severity, "");
|
||||
let span = Span::from(self.file).with_range(range);
|
||||
diag.annotate(Annotation::primary(span).message(message));
|
||||
self.diagnostics.push(diag);
|
||||
self.diagnostics.push(TypeCheckDiagnostic {
|
||||
id: DiagnosticId::Lint(lint.name()),
|
||||
message: message.to_string(),
|
||||
range,
|
||||
severity,
|
||||
file: self.file,
|
||||
secondary_messages: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -464,11 +464,11 @@ impl<'db> SymbolAndQualifiers<'db> {
|
||||
///
|
||||
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
|
||||
/// 2. Else, evaluate `fallback_fn()`:
|
||||
/// 1. If `self` is definitely unbound, return the result of `fallback_fn()`.
|
||||
/// 2. Else, if `fallback` is definitely unbound, return `self`.
|
||||
/// 3. Else, if `self` is possibly unbound and `fallback` is definitely bound,
|
||||
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
|
||||
/// b. Else, if `fallback` is definitely unbound, return `self`.
|
||||
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
|
||||
/// 4. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
|
||||
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
|
||||
#[must_use]
|
||||
pub(crate) fn or_fall_back_to(
|
||||
@@ -657,7 +657,7 @@ fn symbol_from_bindings_impl<'db>(
|
||||
binding,
|
||||
visibility_constraint,
|
||||
narrowing_constraint: _,
|
||||
}) if binding.is_none_or(is_non_exported) => {
|
||||
}) if binding.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
@@ -679,53 +679,6 @@ fn symbol_from_bindings_impl<'db>(
|
||||
visibility_constraints.evaluate(db, predicates, visibility_constraint);
|
||||
|
||||
if static_visibility.is_always_false() {
|
||||
// We found a binding that we have statically determined to not be visible from
|
||||
// the use of the symbol that we are investigating. There are three interesting
|
||||
// cases to consider:
|
||||
//
|
||||
// ```py
|
||||
// def f1():
|
||||
// if False:
|
||||
// x = 1
|
||||
// use(x)
|
||||
//
|
||||
// def f2():
|
||||
// y = 1
|
||||
// return
|
||||
// use(y)
|
||||
//
|
||||
// def f3(flag: bool):
|
||||
// z = 1
|
||||
// if flag:
|
||||
// z = 2
|
||||
// return
|
||||
// use(z)
|
||||
// ```
|
||||
//
|
||||
// In the first case, there is a single binding for `x`, and due to the statically
|
||||
// known `False` condition, it is not visible at the use of `x`. However, we *can*
|
||||
// see/reach the start of the scope from `use(x)`. This means that `x` is unbound
|
||||
// and we should return `None`.
|
||||
//
|
||||
// In the second case, `y` is also not visible at the use of `y`, but here, we can
|
||||
// not see/reach the start of the scope. There is only one path of control flow,
|
||||
// and it passes through that binding of `y` (which we can not see). This implies
|
||||
// that we are in an unreachable section of code. We return `Never` in order to
|
||||
// silence the `unresolve-reference` diagnostic that would otherwise be emitted at
|
||||
// the use of `y`.
|
||||
//
|
||||
// In the third case, we have two bindings for `z`. The first one is visible, so we
|
||||
// consider the case that we now encounter the second binding `z = 2`, which is not
|
||||
// visible due to the early return. We *also* can not see the start of the scope
|
||||
// from `use(z)` because both paths of control flow pass through a binding of `z`.
|
||||
// The `z = 1` binding is visible, and so we are *not* in an unreachable section of
|
||||
// code. However, it is still okay to return `Never` in this case, because we will
|
||||
// union the types of all bindings, and `Never` will be eliminated automatically.
|
||||
|
||||
if unbound_visibility.is_always_false() {
|
||||
// The scope-start is not visible
|
||||
return Some(Type::Never);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -794,7 +747,7 @@ fn symbol_from_declarations_impl<'db>(
|
||||
Some(DeclarationWithConstraint {
|
||||
declaration,
|
||||
visibility_constraint,
|
||||
}) if declaration.is_none_or(is_non_exported) => {
|
||||
}) if declaration.map_or(true, is_non_exported) => {
|
||||
visibility_constraints.evaluate(db, predicates, *visibility_constraint)
|
||||
}
|
||||
_ => Truthiness::AlwaysFalse,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
|
||||
use crate::types::{IntersectionType, KnownClass, Type, TypeVarBoundOrConstraints, UnionType};
|
||||
use crate::types::{IntersectionType, KnownClass, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -485,101 +485,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to simplify any constrained typevars in the intersection:
|
||||
///
|
||||
/// - If the intersection contains a positive entry for exactly one of the constraints, we can
|
||||
/// remove the typevar (effectively replacing it with that one positive constraint).
|
||||
///
|
||||
/// - If the intersection contains negative entries for all but one of the constraints, we can
|
||||
/// remove the negative constraints and replace the typevar with the remaining positive
|
||||
/// constraint.
|
||||
///
|
||||
/// - If the intersection contains negative entries for all of the constraints, the overall
|
||||
/// intersection is `Never`.
|
||||
fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) {
|
||||
let mut to_add = SmallVec::<[Type<'db>; 1]>::new();
|
||||
let mut positive_to_remove = SmallVec::<[usize; 1]>::new();
|
||||
|
||||
for (typevar_index, ty) in self.positive.iter().enumerate() {
|
||||
let Type::TypeVar(typevar) = ty else {
|
||||
continue;
|
||||
};
|
||||
let Some(TypeVarBoundOrConstraints::Constraints(constraints)) =
|
||||
typevar.bound_or_constraints(db)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Determine which constraints appear as positive entries in the intersection. Note
|
||||
// that we shouldn't have duplicate entries in the positive or negative lists, so we
|
||||
// don't need to worry about finding any particular constraint more than once.
|
||||
let constraints = constraints.elements(db);
|
||||
let mut positive_constraint_count = 0;
|
||||
for positive in &self.positive {
|
||||
// This linear search should be fine as long as we don't encounter typevars with
|
||||
// thousands of constraints.
|
||||
positive_constraint_count += constraints
|
||||
.iter()
|
||||
.filter(|c| c.is_subtype_of(db, *positive))
|
||||
.count();
|
||||
}
|
||||
|
||||
// If precisely one constraint appears as a positive element, we can replace the
|
||||
// typevar with that positive constraint.
|
||||
if positive_constraint_count == 1 {
|
||||
positive_to_remove.push(typevar_index);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which constraints appear as negative entries in the intersection.
|
||||
let mut to_remove = Vec::with_capacity(constraints.len());
|
||||
let mut remaining_constraints: Vec<_> = constraints.iter().copied().map(Some).collect();
|
||||
for (negative_index, negative) in self.negative.iter().enumerate() {
|
||||
// This linear search should be fine as long as we don't encounter typevars with
|
||||
// thousands of constraints.
|
||||
let matching_constraints = constraints
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.is_subtype_of(db, *negative));
|
||||
for (constraint_index, _) in matching_constraints {
|
||||
to_remove.push(negative_index);
|
||||
remaining_constraints[constraint_index] = None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut iter = remaining_constraints.into_iter().flatten();
|
||||
let Some(remaining_constraint) = iter.next() else {
|
||||
// All of the typevar constraints have been removed, so the entire intersection is
|
||||
// `Never`.
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
};
|
||||
|
||||
let more_than_one_remaining_constraint = iter.next().is_some();
|
||||
if more_than_one_remaining_constraint {
|
||||
// This typevar cannot be simplified.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only one typevar constraint remains. Remove all of the negative constraints, and
|
||||
// replace the typevar itself with the remaining positive constraint.
|
||||
to_add.push(remaining_constraint);
|
||||
positive_to_remove.push(typevar_index);
|
||||
}
|
||||
|
||||
// We don't need to sort the positive list, since we only append to it in increasing order.
|
||||
for index in positive_to_remove.into_iter().rev() {
|
||||
self.positive.swap_remove_index(index);
|
||||
}
|
||||
|
||||
for remaining_constraint in to_add {
|
||||
self.add_positive(db, remaining_constraint);
|
||||
}
|
||||
}
|
||||
|
||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.simplify_constrained_typevars(db);
|
||||
match (self.positive.len(), self.negative.len()) {
|
||||
(0, 0) => Type::object(db),
|
||||
(1, 0) => self.positive[0],
|
||||
|
||||
@@ -18,8 +18,8 @@ use crate::types::diagnostic::{
|
||||
};
|
||||
use crate::types::signatures::{Parameter, ParameterForm};
|
||||
use crate::types::{
|
||||
todo_type, BoundMethodType, ClassLiteralType, FunctionDecorators, KnownClass, KnownFunction,
|
||||
KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind,
|
||||
todo_type, BoundMethodType, CallableType, ClassLiteralType, KnownClass, KnownFunction,
|
||||
KnownInstanceType, UnionType,
|
||||
};
|
||||
use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span};
|
||||
use ruff_python_ast as ast;
|
||||
@@ -210,20 +210,26 @@ impl<'db> Bindings<'db> {
|
||||
};
|
||||
|
||||
match binding_type {
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
|
||||
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
match overload.parameter_types() {
|
||||
[_, Some(owner)] => {
|
||||
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
|
||||
db, function, *owner,
|
||||
)));
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, function, *owner,
|
||||
)),
|
||||
));
|
||||
}
|
||||
[Some(instance), None] => {
|
||||
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function,
|
||||
instance.to_meta_type(db),
|
||||
)));
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function,
|
||||
instance.to_meta_type(db),
|
||||
)),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -231,32 +237,36 @@ impl<'db> Bindings<'db> {
|
||||
if first.is_none(db) {
|
||||
overload.set_return_type(Type::FunctionLiteral(function));
|
||||
} else {
|
||||
overload.set_return_type(Type::BoundMethod(BoundMethodType::new(
|
||||
db, function, *first,
|
||||
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
|
||||
BoundMethodType::new(db, function, *first),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => {
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
|
||||
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||
&& function.decorators(db).len() == 1
|
||||
{
|
||||
match overload.parameter_types() {
|
||||
[_, _, Some(owner)] => {
|
||||
overload.set_return_type(Type::BoundMethod(
|
||||
BoundMethodType::new(db, *function, *owner),
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, *function, *owner,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(instance), None] => {
|
||||
overload.set_return_type(Type::BoundMethod(
|
||||
BoundMethodType::new(
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
*function,
|
||||
instance.to_meta_type(db),
|
||||
),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -267,9 +277,41 @@ impl<'db> Bindings<'db> {
|
||||
[_, Some(instance), _] if instance.is_none(db) => {
|
||||
overload.set_return_type(*function_ty);
|
||||
}
|
||||
|
||||
[_, Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(
|
||||
type_alias,
|
||||
))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
|
||||
if class.is_known(db, KnownClass::TypeAliasType)
|
||||
&& function.name(db) == "__name__" =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(
|
||||
db,
|
||||
type_alias.name(db),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), Some(Type::ClassLiteral(ClassLiteralType { class }))]
|
||||
if class.is_known(db, KnownClass::TypeVar)
|
||||
&& function.name(db) == "__name__" =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(
|
||||
db,
|
||||
typevar.name(db),
|
||||
));
|
||||
}
|
||||
|
||||
[_, Some(_), _]
|
||||
if function
|
||||
.has_known_class_decorator(db, KnownClass::Property) =>
|
||||
{
|
||||
overload.set_return_type(todo_type!("@property"));
|
||||
}
|
||||
|
||||
[_, Some(instance), _] => {
|
||||
overload.set_return_type(Type::BoundMethod(
|
||||
BoundMethodType::new(db, *function, *instance),
|
||||
overload.set_return_type(Type::Callable(
|
||||
CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, *function, *instance,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -279,165 +321,6 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => {
|
||||
match overload.parameter_types() {
|
||||
[Some(property @ Type::PropertyInstance(_)), Some(instance), ..]
|
||||
if instance.is_none(db) =>
|
||||
{
|
||||
overload.set_return_type(*property);
|
||||
}
|
||||
[Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..]
|
||||
if property.getter(db).is_some_and(|getter| {
|
||||
getter
|
||||
.into_function_literal()
|
||||
.is_some_and(|f| f.name(db) == "__name__")
|
||||
}) =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(db, type_alias.name(db)));
|
||||
}
|
||||
[Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(type_var))), ..]
|
||||
if property.getter(db).is_some_and(|getter| {
|
||||
getter
|
||||
.into_function_literal()
|
||||
.is_some_and(|f| f.name(db) == "__name__")
|
||||
}) =>
|
||||
{
|
||||
overload.set_return_type(Type::string_literal(db, type_var.name(db)));
|
||||
}
|
||||
[Some(Type::PropertyInstance(property)), Some(instance), ..] => {
|
||||
if let Some(getter) = property.getter(db) {
|
||||
if let Ok(return_ty) = getter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance]))
|
||||
.map(|binding| binding.return_type(db))
|
||||
{
|
||||
overload.set_return_type(return_ty);
|
||||
} else {
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the getter failed",
|
||||
));
|
||||
overload.set_return_type(Type::unknown());
|
||||
}
|
||||
} else {
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"property has no getter",
|
||||
));
|
||||
overload.set_return_type(Type::Never);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => {
|
||||
match overload.parameter_types() {
|
||||
[Some(instance), ..] if instance.is_none(db) => {
|
||||
overload.set_return_type(Type::PropertyInstance(property));
|
||||
}
|
||||
[Some(instance), ..] => {
|
||||
if let Some(getter) = property.getter(db) {
|
||||
if let Ok(return_ty) = getter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance]))
|
||||
.map(|binding| binding.return_type(db))
|
||||
{
|
||||
overload.set_return_type(return_ty);
|
||||
} else {
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the getter failed",
|
||||
));
|
||||
overload.set_return_type(Type::unknown());
|
||||
}
|
||||
} else {
|
||||
overload.set_return_type(Type::Never);
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"property has no getter",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => {
|
||||
if let [Some(Type::PropertyInstance(property)), Some(instance), Some(value), ..] =
|
||||
overload.parameter_types()
|
||||
{
|
||||
if let Some(setter) = property.setter(db) {
|
||||
if let Err(_call_error) = setter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance, *value]))
|
||||
{
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the setter failed",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
overload
|
||||
.errors
|
||||
.push(BindingError::InternalCallError("property has no setter"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => {
|
||||
if let [Some(instance), Some(value), ..] = overload.parameter_types() {
|
||||
if let Some(setter) = property.setter(db) {
|
||||
if let Err(_call_error) = setter
|
||||
.try_call(db, CallArgumentTypes::positional([*instance, *value]))
|
||||
{
|
||||
overload.errors.push(BindingError::InternalCallError(
|
||||
"calling the setter failed",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
overload
|
||||
.errors
|
||||
.push(BindingError::InternalCallError("property has no setter"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::BoundMethod(bound_method)
|
||||
if bound_method.self_instance(db).is_property_instance() =>
|
||||
{
|
||||
match bound_method.function(db).name(db).as_str() {
|
||||
"setter" => {
|
||||
if let [Some(_), Some(setter)] = overload.parameter_types() {
|
||||
let mut ty_property = bound_method.self_instance(db);
|
||||
if let Type::PropertyInstance(property) = ty_property {
|
||||
ty_property =
|
||||
Type::PropertyInstance(PropertyInstanceType::new(
|
||||
db,
|
||||
property.getter(db),
|
||||
Some(*setter),
|
||||
));
|
||||
}
|
||||
overload.set_return_type(ty_property);
|
||||
}
|
||||
}
|
||||
"getter" => {
|
||||
if let [Some(_), Some(getter)] = overload.parameter_types() {
|
||||
let mut ty_property = bound_method.self_instance(db);
|
||||
if let Type::PropertyInstance(property) = ty_property {
|
||||
ty_property =
|
||||
Type::PropertyInstance(PropertyInstanceType::new(
|
||||
db,
|
||||
Some(*getter),
|
||||
property.setter(db),
|
||||
));
|
||||
}
|
||||
overload.set_return_type(ty_property);
|
||||
}
|
||||
}
|
||||
"deleter" => {
|
||||
// TODO: we do not store deleters yet
|
||||
let ty_property = bound_method.self_instance(db);
|
||||
overload.set_return_type(ty_property);
|
||||
}
|
||||
_ => {
|
||||
// Fall back to typeshed stubs for all other methods
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
@@ -502,13 +385,13 @@ impl<'db> Bindings<'db> {
|
||||
if let Some(len_ty) = first_arg.len(db) {
|
||||
overload.set_return_type(len_ty);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some(KnownFunction::Repr) => {
|
||||
if let [Some(first_arg)] = overload.parameter_types() {
|
||||
overload.set_return_type(first_arg.repr(db));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some(KnownFunction::Cast) => {
|
||||
@@ -587,14 +470,6 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownClass::Property) => {
|
||||
if let [getter, setter, ..] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::PropertyInstance(
|
||||
PropertyInstanceType::new(db, *getter, *setter),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -1060,29 +935,19 @@ impl<'db> CallableDescription<'db> {
|
||||
kind: "class",
|
||||
name: class_type.class().name(db),
|
||||
}),
|
||||
Type::BoundMethod(bound_method) => Some(CallableDescription {
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => Some(CallableDescription {
|
||||
kind: "bound method",
|
||||
name: bound_method.function(db).name(db),
|
||||
}),
|
||||
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
Some(CallableDescription {
|
||||
kind: "method wrapper `__get__` of function",
|
||||
name: function.name(db),
|
||||
})
|
||||
}
|
||||
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
|
||||
Some(CallableDescription {
|
||||
kind: "method wrapper",
|
||||
name: "`__get__` of property",
|
||||
})
|
||||
}
|
||||
Type::WrapperDescriptor(kind) => Some(CallableDescription {
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => Some(CallableDescription {
|
||||
kind: "wrapper descriptor",
|
||||
name: match kind {
|
||||
WrapperDescriptorKind::FunctionTypeDunderGet => "FunctionType.__get__",
|
||||
WrapperDescriptorKind::PropertyDunderGet => "property.__get__",
|
||||
WrapperDescriptorKind::PropertyDunderSet => "property.__set__",
|
||||
},
|
||||
name: "FunctionType.__get__",
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
@@ -1170,10 +1035,6 @@ pub(crate) enum BindingError<'db> {
|
||||
argument_index: Option<usize>,
|
||||
parameter: ParameterContext,
|
||||
},
|
||||
/// The call itself might be well constructed, but an error occurred while evaluating the call.
|
||||
/// We use this variant to report errors in `property.__get__` and `property.__set__`, which
|
||||
/// can occur when the call to the underlying getter/setter fails.
|
||||
InternalCallError(&'static str),
|
||||
}
|
||||
|
||||
impl<'db> BindingError<'db> {
|
||||
@@ -1200,11 +1061,13 @@ impl<'db> BindingError<'db> {
|
||||
None
|
||||
}
|
||||
}
|
||||
Type::BoundMethod(bound_method) => Self::parameter_span_from_index(
|
||||
db,
|
||||
Type::FunctionLiteral(bound_method.function(db)),
|
||||
parameter_index,
|
||||
),
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => {
|
||||
Self::parameter_span_from_index(
|
||||
db,
|
||||
Type::FunctionLiteral(bound_method.function(db)),
|
||||
parameter_index,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1247,7 +1110,7 @@ impl<'db> BindingError<'db> {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
&messages,
|
||||
messages,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1322,21 +1185,6 @@ impl<'db> BindingError<'db> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Self::InternalCallError(reason) => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
Self::get_node(node, None),
|
||||
format_args!(
|
||||
"Call{} failed: {reason}",
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use super::{
|
||||
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
|
||||
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
|
||||
TypeQualifiers, TypeVarInstance,
|
||||
};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::{
|
||||
module_resolver::file_to_module,
|
||||
semantic_index::{
|
||||
@@ -24,10 +18,16 @@ use crate::{
|
||||
};
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools as _;
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{
|
||||
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
|
||||
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
|
||||
TypeQualifiers, TypeVarInstance,
|
||||
};
|
||||
|
||||
/// Representation of a runtime class object.
|
||||
///
|
||||
/// Does not in itself represent a type,
|
||||
@@ -43,14 +43,52 @@ pub struct Class<'db> {
|
||||
pub(crate) known: Option<KnownClass>,
|
||||
}
|
||||
|
||||
fn explicit_bases_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &[Type<'db>],
|
||||
_count: u32,
|
||||
_self: Class<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Box<[Type<'db>]>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn explicit_bases_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Box<[Type<'db>]> {
|
||||
Box::default()
|
||||
}
|
||||
|
||||
fn try_mro_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<Mro<'db>, MroError<'db>>,
|
||||
_count: u32,
|
||||
_self: Class<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn try_mro_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_: Class<'db>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Ok(Mro::from_error(db, self_))
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
|
||||
fn inheritance_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Option<InheritanceCycle>,
|
||||
_count: u32,
|
||||
_self: Class<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Option<InheritanceCycle>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn inheritance_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Option<InheritanceCycle> {
|
||||
None
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> Class<'db> {
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
index.expect_single_definition(scope.node(db).expect_class())
|
||||
}
|
||||
|
||||
/// Return `true` if this class represents `known_class`
|
||||
pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
self.known(db) == Some(known_class)
|
||||
@@ -115,15 +153,6 @@ impl<'db> Class<'db> {
|
||||
self.body_scope(db).node(db).expect_class()
|
||||
}
|
||||
|
||||
/// Returns the file range of the class's name.
|
||||
pub fn focus_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.node(db).name.range)
|
||||
}
|
||||
|
||||
pub fn full_range(self, db: &dyn Db) -> FileRange {
|
||||
FileRange::new(self.file(db), self.node(db).range)
|
||||
}
|
||||
|
||||
/// Return the types of the decorators on this class
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
@@ -201,7 +230,8 @@ impl<'db> Class<'db> {
|
||||
.find_keyword("metaclass")?
|
||||
.value;
|
||||
|
||||
let class_definition = self.definition(db);
|
||||
let class_definition =
|
||||
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
|
||||
|
||||
Some(definition_expression_type(
|
||||
db,
|
||||
@@ -701,50 +731,6 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn explicit_bases_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &[Type<'db>],
|
||||
_count: u32,
|
||||
_self: Class<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Box<[Type<'db>]>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn explicit_bases_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Box<[Type<'db>]> {
|
||||
Box::default()
|
||||
}
|
||||
|
||||
fn try_mro_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<Mro<'db>, MroError<'db>>,
|
||||
_count: u32,
|
||||
_self: Class<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn try_mro_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_: Class<'db>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Ok(Mro::from_error(db, self_))
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
|
||||
fn inheritance_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Option<InheritanceCycle>,
|
||||
_count: u32,
|
||||
_self: Class<'db>,
|
||||
) -> salsa::CycleRecoveryAction<Option<InheritanceCycle>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
fn inheritance_cycle_initial<'db>(_db: &'db dyn Db, _self: Class<'db>) -> Option<InheritanceCycle> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(super) enum InheritanceCycle {
|
||||
/// The class is cyclically defined and is a participant in the cycle.
|
||||
@@ -768,7 +754,7 @@ pub struct ClassLiteralType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> ClassLiteralType<'db> {
|
||||
pub(super) fn class(self) -> Class<'db> {
|
||||
pub(crate) fn class(self) -> Class<'db> {
|
||||
self.class
|
||||
}
|
||||
|
||||
@@ -825,7 +811,6 @@ pub enum KnownClass {
|
||||
Bool,
|
||||
Object,
|
||||
Bytes,
|
||||
Bytearray,
|
||||
Type,
|
||||
Int,
|
||||
Float,
|
||||
@@ -842,9 +827,6 @@ pub enum KnownClass {
|
||||
BaseException,
|
||||
BaseExceptionGroup,
|
||||
Classmethod,
|
||||
Super,
|
||||
// enum
|
||||
Enum,
|
||||
// Types
|
||||
GenericAlias,
|
||||
ModuleType,
|
||||
@@ -855,13 +837,10 @@ pub enum KnownClass {
|
||||
// Typeshed
|
||||
NoneType, // Part of `types` for Python >= 3.10
|
||||
// Typing
|
||||
Any,
|
||||
StdlibAlias,
|
||||
SpecialForm,
|
||||
TypeVar,
|
||||
ParamSpec,
|
||||
ParamSpecArgs,
|
||||
ParamSpecKwargs,
|
||||
TypeVarTuple,
|
||||
TypeAliasType,
|
||||
NoDefaultType,
|
||||
@@ -880,7 +859,6 @@ pub enum KnownClass {
|
||||
// Exposed as `types.EllipsisType` on Python >=3.10;
|
||||
// backported as `builtins.ellipsis` by typeshed on Python <=3.9
|
||||
EllipsisType,
|
||||
NotImplementedType,
|
||||
}
|
||||
|
||||
impl<'db> KnownClass {
|
||||
@@ -911,16 +889,13 @@ impl<'db> KnownClass {
|
||||
| Self::TypeAliasType
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::WrapperDescriptorType
|
||||
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
|
||||
|
||||
Self::NoneType => Truthiness::AlwaysFalse,
|
||||
|
||||
Self::Any
|
||||
| Self::BaseException
|
||||
Self::BaseException
|
||||
| Self::Object
|
||||
| Self::OrderedDict
|
||||
| Self::BaseExceptionGroup
|
||||
@@ -936,7 +911,6 @@ impl<'db> KnownClass {
|
||||
| Self::Int
|
||||
| Self::Type
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::FrozenSet
|
||||
| Self::Range
|
||||
| Self::Property
|
||||
@@ -950,23 +924,15 @@ impl<'db> KnownClass {
|
||||
| Self::Deque
|
||||
| Self::Float
|
||||
| Self::Sized
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
||||
// and raises a `TypeError` in Python >=3.14
|
||||
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
||||
| Self::NotImplementedType
|
||||
| Self::Classmethod => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'static str {
|
||||
match self {
|
||||
Self::Any => "Any",
|
||||
Self::Bool => "bool",
|
||||
Self::Object => "object",
|
||||
Self::Bytes => "bytes",
|
||||
Self::Bytearray => "bytearray",
|
||||
Self::Tuple => "tuple",
|
||||
Self::Int => "int",
|
||||
Self::Float => "float",
|
||||
@@ -993,8 +959,6 @@ impl<'db> KnownClass {
|
||||
Self::SpecialForm => "_SpecialForm",
|
||||
Self::TypeVar => "TypeVar",
|
||||
Self::ParamSpec => "ParamSpec",
|
||||
Self::ParamSpecArgs => "ParamSpecArgs",
|
||||
Self::ParamSpecKwargs => "ParamSpecKwargs",
|
||||
Self::TypeVarTuple => "TypeVarTuple",
|
||||
Self::TypeAliasType => "TypeAliasType",
|
||||
Self::NoDefaultType => "_NoDefaultType",
|
||||
@@ -1006,8 +970,6 @@ impl<'db> KnownClass {
|
||||
Self::Deque => "deque",
|
||||
Self::Sized => "Sized",
|
||||
Self::OrderedDict => "OrderedDict",
|
||||
Self::Enum => "Enum",
|
||||
Self::Super => "super",
|
||||
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
|
||||
Self::StdlibAlias => "_Alias",
|
||||
// This is the name the type of `sys.version_info` has in typeshed,
|
||||
@@ -1025,7 +987,6 @@ impl<'db> KnownClass {
|
||||
"ellipsis"
|
||||
}
|
||||
}
|
||||
Self::NotImplementedType => "_NotImplementedType",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1144,7 +1105,6 @@ impl<'db> KnownClass {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1160,10 +1120,8 @@ impl<'db> KnownClass {
|
||||
| Self::Classmethod
|
||||
| Self::Slice
|
||||
| Self::Range
|
||||
| Self::Super
|
||||
| Self::Property => KnownModule::Builtins,
|
||||
Self::VersionInfo => KnownModule::Sys,
|
||||
Self::Enum => KnownModule::Enum,
|
||||
Self::GenericAlias
|
||||
| Self::ModuleType
|
||||
| Self::FunctionType
|
||||
@@ -1171,18 +1129,14 @@ impl<'db> KnownClass {
|
||||
| Self::MethodWrapperType
|
||||
| Self::WrapperDescriptorType => KnownModule::Types,
|
||||
Self::NoneType => KnownModule::Typeshed,
|
||||
Self::Any
|
||||
| Self::SpecialForm
|
||||
Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::StdlibAlias
|
||||
| Self::SupportsIndex
|
||||
| Self::Sized => KnownModule::Typing,
|
||||
Self::TypeAliasType
|
||||
| Self::TypeVarTuple
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::NewType => KnownModule::TypingExtensions,
|
||||
Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec | Self::NewType => {
|
||||
KnownModule::TypingExtensions
|
||||
}
|
||||
Self::NoDefaultType => {
|
||||
let python_version = Program::get(db).python_version(db);
|
||||
|
||||
@@ -1204,7 +1158,6 @@ impl<'db> KnownClass {
|
||||
KnownModule::Builtins
|
||||
}
|
||||
}
|
||||
Self::NotImplementedType => KnownModule::Builtins,
|
||||
Self::ChainMap
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
@@ -1220,14 +1173,11 @@ impl<'db> KnownClass {
|
||||
| Self::NoDefaultType
|
||||
| Self::VersionInfo
|
||||
| Self::EllipsisType
|
||||
| Self::TypeAliasType
|
||||
| Self::NotImplementedType => true,
|
||||
| Self::TypeAliasType => true,
|
||||
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1260,12 +1210,8 @@ impl<'db> KnownClass {
|
||||
| Self::StdlibAlias
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
| Self::NewType => false,
|
||||
}
|
||||
}
|
||||
@@ -1274,19 +1220,17 @@ impl<'db> KnownClass {
|
||||
///
|
||||
/// A singleton class is a class where it is known that only one instance can ever exist at runtime.
|
||||
pub(super) const fn is_singleton(self) -> bool {
|
||||
// TODO there are other singleton types (NotImplementedType -- any others?)
|
||||
match self {
|
||||
Self::NoneType
|
||||
| Self::EllipsisType
|
||||
| Self::NoDefaultType
|
||||
| Self::VersionInfo
|
||||
| Self::TypeAliasType
|
||||
| Self::NotImplementedType => true,
|
||||
| Self::TypeAliasType => true,
|
||||
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Tuple
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1319,12 +1263,8 @@ impl<'db> KnownClass {
|
||||
| Self::Classmethod
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
| Self::NewType => false,
|
||||
}
|
||||
}
|
||||
@@ -1337,11 +1277,9 @@ impl<'db> KnownClass {
|
||||
// We assert that this match is exhaustive over the right-hand side in the unit test
|
||||
// `known_class_roundtrip_from_str()`
|
||||
let candidate = match class_name {
|
||||
"Any" => Self::Any,
|
||||
"bool" => Self::Bool,
|
||||
"object" => Self::Object,
|
||||
"bytes" => Self::Bytes,
|
||||
"bytearray" => Self::Bytearray,
|
||||
"tuple" => Self::Tuple,
|
||||
"type" => Self::Type,
|
||||
"int" => Self::Int,
|
||||
@@ -1369,8 +1307,6 @@ impl<'db> KnownClass {
|
||||
"TypeAliasType" => Self::TypeAliasType,
|
||||
"TypeVar" => Self::TypeVar,
|
||||
"ParamSpec" => Self::ParamSpec,
|
||||
"ParamSpecArgs" => Self::ParamSpecArgs,
|
||||
"ParamSpecKwargs" => Self::ParamSpecKwargs,
|
||||
"TypeVarTuple" => Self::TypeVarTuple,
|
||||
"ChainMap" => Self::ChainMap,
|
||||
"Counter" => Self::Counter,
|
||||
@@ -1382,8 +1318,6 @@ impl<'db> KnownClass {
|
||||
"_NoDefaultType" => Self::NoDefaultType,
|
||||
"SupportsIndex" => Self::SupportsIndex,
|
||||
"Sized" => Self::Sized,
|
||||
"Enum" => Self::Enum,
|
||||
"super" => Self::Super,
|
||||
"_version_info" => Self::VersionInfo,
|
||||
"ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => {
|
||||
Self::EllipsisType
|
||||
@@ -1391,7 +1325,6 @@ impl<'db> KnownClass {
|
||||
"EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => {
|
||||
Self::EllipsisType
|
||||
}
|
||||
"_NotImplementedType" => Self::NotImplementedType,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -1403,11 +1336,9 @@ impl<'db> KnownClass {
|
||||
/// Return `true` if the module of `self` matches `module`
|
||||
fn check_module(self, db: &'db dyn Db, module: KnownModule) -> bool {
|
||||
match self {
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Bytearray
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
@@ -1437,9 +1368,6 @@ impl<'db> KnownClass {
|
||||
| Self::FunctionType
|
||||
| Self::MethodType
|
||||
| Self::MethodWrapperType
|
||||
| Self::Enum
|
||||
| Self::Super
|
||||
| Self::NotImplementedType
|
||||
| Self::WrapperDescriptorType => module == self.canonical_module(db),
|
||||
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
|
||||
Self::SpecialForm
|
||||
@@ -1448,8 +1376,6 @@ impl<'db> KnownClass {
|
||||
| Self::NoDefaultType
|
||||
| Self::SupportsIndex
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::Sized
|
||||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
@@ -1531,9 +1457,6 @@ pub enum KnownInstanceType<'db> {
|
||||
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
|
||||
Never,
|
||||
/// The symbol `typing.Any` (which can also be found as `typing_extensions.Any`)
|
||||
/// This is not used since typeshed switched to representing `Any` as a class; now we use
|
||||
/// `KnownClass::Any` instead. But we still support the old `Any = object()` representation, at
|
||||
/// least for now. TODO maybe remove?
|
||||
Any,
|
||||
/// The symbol `typing.Tuple` (which can also be found as `typing_extensions.Tuple`)
|
||||
Tuple,
|
||||
@@ -1575,8 +1498,8 @@ pub enum KnownInstanceType<'db> {
|
||||
Intersection,
|
||||
/// The symbol `knot_extensions.TypeOf`
|
||||
TypeOf,
|
||||
/// The symbol `knot_extensions.CallableTypeOf`
|
||||
CallableTypeOf,
|
||||
/// The symbol `knot_extensions.CallableTypeFromFunction`
|
||||
CallableTypeFromFunction,
|
||||
|
||||
// Various special forms, special aliases and type qualifiers that we don't yet understand
|
||||
// (all currently inferred as TODO in most contexts):
|
||||
@@ -1639,7 +1562,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Not
|
||||
| Self::Intersection
|
||||
| Self::TypeOf
|
||||
| Self::CallableTypeOf => Truthiness::AlwaysTrue,
|
||||
| Self::CallableTypeFromFunction => Truthiness::AlwaysTrue,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1686,7 +1609,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::Not => "knot_extensions.Not",
|
||||
Self::Intersection => "knot_extensions.Intersection",
|
||||
Self::TypeOf => "knot_extensions.TypeOf",
|
||||
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
|
||||
Self::CallableTypeFromFunction => "knot_extensions.CallableTypeFromFunction",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1730,7 +1653,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::TypeOf => KnownClass::SpecialForm,
|
||||
Self::Not => KnownClass::SpecialForm,
|
||||
Self::Intersection => KnownClass::SpecialForm,
|
||||
Self::CallableTypeOf => KnownClass::SpecialForm,
|
||||
Self::CallableTypeFromFunction => KnownClass::SpecialForm,
|
||||
Self::Unknown => KnownClass::Object,
|
||||
Self::AlwaysTruthy => KnownClass::Object,
|
||||
Self::AlwaysFalsy => KnownClass::Object,
|
||||
@@ -1795,7 +1718,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
"Not" => Self::Not,
|
||||
"Intersection" => Self::Intersection,
|
||||
"TypeOf" => Self::TypeOf,
|
||||
"CallableTypeOf" => Self::CallableTypeOf,
|
||||
"CallableTypeFromFunction" => Self::CallableTypeFromFunction,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -1852,7 +1775,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
| Self::Not
|
||||
| Self::Intersection
|
||||
| Self::TypeOf
|
||||
| Self::CallableTypeOf => module.is_knot_extensions(),
|
||||
| Self::CallableTypeFromFunction => module.is_knot_extensions(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,22 +61,14 @@ impl<'db> ClassBase<'db> {
|
||||
pub(super) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
|
||||
Type::ClassLiteral(literal) => Some(if literal.class().is_known(db, KnownClass::Any) {
|
||||
Self::Dynamic(DynamicType::Any)
|
||||
} else {
|
||||
Self::Class(literal.class())
|
||||
}),
|
||||
Type::ClassLiteral(literal) => Some(Self::Class(literal.class())),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
|
||||
Type::PropertyInstance(_) => None,
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
@@ -85,7 +77,6 @@ impl<'db> ClassBase<'db> {
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
@@ -112,7 +103,7 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::Not
|
||||
| KnownInstanceType::Intersection
|
||||
| KnownInstanceType::TypeOf
|
||||
| KnownInstanceType::CallableTypeOf
|
||||
| KnownInstanceType::CallableTypeFromFunction
|
||||
| KnownInstanceType::AlwaysTruthy
|
||||
| KnownInstanceType::AlwaysFalsy => None,
|
||||
KnownInstanceType::Unknown => Some(Self::unknown()),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user