Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
4247a4d90c [red-knot] Do not merge: Run ecosystem checks with 3.9 2025-04-15 09:48:35 +02:00
610 changed files with 11957 additions and 32734 deletions

6
.gitattributes vendored
View File

@@ -12,12 +12,6 @@ crates/ruff_python_parser/resources/invalid/re_lexing/line_continuation_windows_
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_windows_eol.py text eol=crlf
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text eol=cr
crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py text eol=lf
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf
crates/ruff_python_parser/resources/inline linguist-generated=true
ruff.schema.json -diff linguist-generated=true text=auto eol=lf

View File

@@ -6,6 +6,5 @@ self-hosted-runner:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- depot-ubuntu-22.04-32
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16

View File

@@ -49,7 +49,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
command: sdist
args: --out dist
@@ -79,7 +79,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: x86_64
args: --release --locked --out dist
@@ -121,7 +121,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: aarch64
args: --release --locked --out dist
@@ -177,7 +177,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -230,7 +230,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -304,7 +304,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: ${{ matrix.platform.target }}
manylinux: auto
@@ -370,14 +370,14 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --locked --out dist
- name: "Test wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3
uses: addnab/docker-run-action@v3
with:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io
@@ -435,7 +435,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2

View File

@@ -79,7 +79,7 @@ jobs:
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
with:
context: .
platforms: ${{ matrix.platform }}
@@ -231,7 +231,7 @@ jobs:
${{ env.TAG_PATTERNS }}
- name: Build and push
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -237,13 +237,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-insta
- name: Red-knot mdtests (GitHub annotations)
@@ -276,10 +276,6 @@ jobs:
with:
name: ruff
path: target/debug/ruff
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: red_knot
path: target/debug/red_knot
cargo-test-linux-release:
name: "cargo test (linux, release)"
@@ -295,13 +291,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -324,7 +320,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -350,7 +346,7 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 20
cache: "npm"
@@ -380,7 +376,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Build"
run: cargo build --release --locked
@@ -405,13 +401,13 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -437,7 +433,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
uses: cargo-bins/cargo-binstall@main
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -459,7 +455,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
name: Download Ruff binary to test
id: download-cached-binary
@@ -636,53 +632,6 @@ jobs:
name: ecosystem-result
path: ecosystem-result
fuzz-redknot:
name: "Fuzz for new red-knot panics"
runs-on: depot-ubuntu-22.04-16
needs:
- cargo-test-linux
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.red_knot == 'true' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
name: Download new red-knot binary
id: redknot-new
with:
name: red_knot
path: target/debug
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download baseline red-knot binary
with:
name: red_knot
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: Fuzz
env:
FORCE_COLOR: 1
NEW_REDKNOT: ${{ steps.redknot-new.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x "${PWD}/red_knot" "${NEW_REDKNOT}/red_knot"
(
uvx \
--python="${PYTHON_VERSION}" \
--from=./python/py-fuzzer \
fuzz \
--test-executable="${NEW_REDKNOT}/red_knot" \
--baseline-executable="${PWD}/red_knot" \
--only-new-bugs \
--bin=red_knot \
0-500
)
cargo-shear:
name: "cargo shear"
runs-on: ubuntu-latest
@@ -692,7 +641,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
- uses: cargo-bins/cargo-binstall@main
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -713,7 +662,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
with:
args: --out dist
- name: "Test wheel"
@@ -732,7 +681,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- name: "Cache pre-commit"
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
@@ -771,7 +720,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -872,7 +821,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 22
cache: "npm"
@@ -908,7 +857,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
with:
tool: cargo-codspeed

View File

@@ -34,11 +34,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -36,7 +36,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release

View File

@@ -21,12 +21,11 @@ env:
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: depot-ubuntu-22.04-32
runs-on: depot-ubuntu-22.04-16
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -36,22 +35,23 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
- name: Run mypy_primer
shell: bash
run: |
cd ruff
PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)"
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
@@ -62,16 +62,13 @@ jobs:
cd ..
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@b83b9eade0b7ed2f4b9b129b163acac1ecb48f71" \
mypy_primer \
uvx mypy_primer \
--repo ruff \
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector "/($PRIMER_SELECTOR)\$" \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy|werkzeug|bidict|async-utils)$' \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 22
cache: "npm"

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
pattern: wheels-*

View File

@@ -45,7 +45,7 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -40,7 +40,6 @@ permissions:
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
workflow_dispatch:
inputs:
tag:
@@ -61,7 +60,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
submodules: recursive
@@ -69,9 +68,9 @@ 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.4/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -87,7 +86,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -124,19 +123,19 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
with:
pattern: artifacts-*
path: target/distrib/
@@ -154,7 +153,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: artifacts-build-global
path: |
@@ -175,19 +174,19 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
with:
pattern: artifacts-*
path: target/distrib/
@@ -201,7 +200,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -251,13 +250,13 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
submodules: recursive
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
with:
pattern: artifacts-*
path: artifacts

View File

@@ -79,7 +79,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.7
rev: v0.11.5
hooks:
- id: ruff-format
- id: ruff
@@ -97,7 +97,7 @@ 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.6.0
rev: v1.5.2
hooks:
- id: zizmor

View File

@@ -1,77 +1,5 @@
# Changelog
## 0.11.8
### Preview features
- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR302`, `AIR311`) ([#17553](https://github.com/astral-sh/ruff/pull/17553), [#17570](https://github.com/astral-sh/ruff/pull/17570), [#17571](https://github.com/astral-sh/ruff/pull/17571))
- \[`airflow`\] Extend `AIR301` rule ([#17598](https://github.com/astral-sh/ruff/pull/17598))
- \[`airflow`\] Update existing `AIR302` rules with better suggestions ([#17542](https://github.com/astral-sh/ruff/pull/17542))
- \[`refurb`\] Mark fix as safe for `readlines-in-for` (`FURB129`) ([#17644](https://github.com/astral-sh/ruff/pull/17644))
- [syntax-errors] `nonlocal` declaration at module level ([#17559](https://github.com/astral-sh/ruff/pull/17559))
- [syntax-errors] Detect single starred expression assignment `x = *y` ([#17624](https://github.com/astral-sh/ruff/pull/17624))
### Bug fixes
- \[`flake8-pyi`\] Ensure `Literal[None,] | Literal[None,]` is not autofixed to `None | None` (`PYI061`) ([#17659](https://github.com/astral-sh/ruff/pull/17659))
- \[`flake8-use-pathlib`\] Avoid suggesting `Path.iterdir()` for `os.listdir` with file descriptor (`PTH208`) ([#17715](https://github.com/astral-sh/ruff/pull/17715))
- \[`flake8-use-pathlib`\] Fix `PTH104` false positive when `rename` is passed a file descriptor ([#17712](https://github.com/astral-sh/ruff/pull/17712))
- \[`flake8-use-pathlib`\] Fix `PTH116` false positive when `stat` is passed a file descriptor ([#17709](https://github.com/astral-sh/ruff/pull/17709))
- \[`flake8-use-pathlib`\] Fix `PTH123` false positive when `open` is passed a file descriptor from a function call ([#17705](https://github.com/astral-sh/ruff/pull/17705))
- \[`pycodestyle`\] Fix duplicated diagnostic in `E712` ([#17651](https://github.com/astral-sh/ruff/pull/17651))
- \[`pylint`\] Detect `global` declarations in module scope (`PLE0118`) ([#17411](https://github.com/astral-sh/ruff/pull/17411))
- [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific ([#17460](https://github.com/astral-sh/ruff/pull/17460))
### Configuration
- Add option to disable `typing_extensions` imports ([#17611](https://github.com/astral-sh/ruff/pull/17611))
### Documentation
- Fix example syntax for the `lint.pydocstyle.ignore-var-parameters` option ([#17740](https://github.com/astral-sh/ruff/pull/17740))
- Add fix safety sections (`ASYNC116`, `FLY002`, `D200`, `RUF005`, `RUF017`, `RUF027`, `RUF028`, `RUF057`) ([#17497](https://github.com/astral-sh/ruff/pull/17497), [#17496](https://github.com/astral-sh/ruff/pull/17496), [#17502](https://github.com/astral-sh/ruff/pull/17502), [#17484](https://github.com/astral-sh/ruff/pull/17484), [#17480](https://github.com/astral-sh/ruff/pull/17480), [#17485](https://github.com/astral-sh/ruff/pull/17485), [#17722](https://github.com/astral-sh/ruff/pull/17722), [#17483](https://github.com/astral-sh/ruff/pull/17483))
### Other changes
- Add Python 3.14 to configuration options ([#17647](https://github.com/astral-sh/ruff/pull/17647))
- Make syntax error for unparenthesized except tuples version specific to before 3.14 ([#17660](https://github.com/astral-sh/ruff/pull/17660))
## 0.11.7
### Preview features
- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR301`) ([#17355](https://github.com/astral-sh/ruff/pull/17355))
- \[`perflint`\] Implement fix for `manual-dict-comprehension` (`PERF403`) ([#16719](https://github.com/astral-sh/ruff/pull/16719))
- [syntax-errors] Make duplicate parameter names a semantic error ([#17131](https://github.com/astral-sh/ruff/pull/17131))
### Bug fixes
- \[`airflow`\] Fix typos in provider package names (`AIR302`, `AIR312`) ([#17574](https://github.com/astral-sh/ruff/pull/17574))
- \[`flake8-type-checking`\] Visit keyword arguments in checks involving `typing.cast`/`typing.NewType` arguments ([#17538](https://github.com/astral-sh/ruff/pull/17538))
- \[`pyupgrade`\] Preserve parenthesis when fixing native literals containing newlines (`UP018`) ([#17220](https://github.com/astral-sh/ruff/pull/17220))
- \[`refurb`\] Mark the `FURB161` fix unsafe except for integers and booleans ([#17240](https://github.com/astral-sh/ruff/pull/17240))
### Rule changes
- \[`perflint`\] Allow list function calls to be replaced with a comprehension (`PERF401`) ([#17519](https://github.com/astral-sh/ruff/pull/17519))
- \[`pycodestyle`\] Auto-fix redundant boolean comparison (`E712`) ([#17090](https://github.com/astral-sh/ruff/pull/17090))
- \[`pylint`\] make fix unsafe if delete comments (`PLR1730`) ([#17459](https://github.com/astral-sh/ruff/pull/17459))
### Documentation
- Add fix safety sections to docs for several rules ([#17410](https://github.com/astral-sh/ruff/pull/17410),[#17440](https://github.com/astral-sh/ruff/pull/17440),[#17441](https://github.com/astral-sh/ruff/pull/17441),[#17443](https://github.com/astral-sh/ruff/pull/17443),[#17444](https://github.com/astral-sh/ruff/pull/17444))
## 0.11.6
### Preview features
- Avoid adding whitespace to the end of a docstring after an escaped quote ([#17216](https://github.com/astral-sh/ruff/pull/17216))
- \[`airflow`\] Extract `AIR311` from `AIR301` rules (`AIR301`, `AIR311`) ([#17310](https://github.com/astral-sh/ruff/pull/17310), [#17422](https://github.com/astral-sh/ruff/pull/17422))
### Bug fixes
- Raise syntax error when `\` is at end of file ([#17409](https://github.com/astral-sh/ruff/pull/17409))
## 0.11.5
### Preview features

197
Cargo.lock generated
View File

@@ -128,9 +128,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "argfile"
@@ -216,9 +216,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.12.0"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
dependencies = [
"memchr",
"regex-automata 0.4.9",
@@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.37"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.37"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
dependencies = [
"anstream",
"anstyle",
@@ -394,7 +394,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -487,7 +487,21 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
@@ -710,7 +724,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -721,7 +735,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -791,7 +805,7 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -823,7 +837,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -1309,7 +1323,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -1474,7 +1488,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -1539,43 +1553,26 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.10"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
]
[[package]]
name = "jiff-static"
version = "0.2.10"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
"syn 2.0.100",
]
[[package]]
@@ -1631,9 +1628,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libcst"
@@ -1657,14 +1654,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa96ed35d0dccc67cf7ba49350cb86de3dcb1d072a7ab28f99117f19d874953"
dependencies = [
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
name = "libmimalloc-sys"
version = "0.1.42"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
dependencies = [
"cc",
"libc",
@@ -1800,9 +1797,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mimalloc"
version = "0.1.46"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
dependencies = [
"libmimalloc-sys",
]
@@ -2166,7 +2163,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -2235,7 +2232,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -2313,9 +2310,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
@@ -2406,12 +2403,13 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy",
]
[[package]]
@@ -2478,6 +2476,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"argfile",
"chrono",
"clap",
"colored 3.0.0",
"countme",
@@ -2486,7 +2485,6 @@ dependencies = [
"filetime",
"insta",
"insta-cmd",
"jiff",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
@@ -2558,7 +2556,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"camino",
"compact_str",
"compact_str 0.9.0",
"countme",
"dir-test",
"drop_bomb",
@@ -2758,7 +2756,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.8"
version = "0.11.5"
dependencies = [
"anyhow",
"argfile",
@@ -2766,6 +2764,7 @@ dependencies = [
"bincode",
"bitflags 2.9.0",
"cachedir",
"chrono",
"clap",
"clap_complete_command",
"clearscreen",
@@ -2778,7 +2777,6 @@ dependencies = [
"insta-cmd",
"is-macro",
"itertools 0.14.0",
"jiff",
"log",
"mimalloc",
"notify",
@@ -2865,7 +2863,6 @@ dependencies = [
name = "ruff_db"
version = "0.0.0"
dependencies = [
"anstyle",
"camino",
"countme",
"dashmap 6.1.0",
@@ -2994,11 +2991,12 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.8"
version = "0.11.5"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.9.0",
"chrono",
"clap",
"colored 3.0.0",
"fern",
@@ -3009,7 +3007,6 @@ dependencies = [
"is-macro",
"is-wsl",
"itertools 0.14.0",
"jiff",
"libcst",
"log",
"memchr",
@@ -3061,7 +3058,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3070,7 +3067,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
"rand 0.9.1",
"rand 0.9.0",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -3088,7 +3085,7 @@ version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.0",
"compact_str",
"compact_str 0.9.0",
"is-macro",
"itertools 0.14.0",
"memchr",
@@ -3186,7 +3183,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"bstr",
"compact_str",
"compact_str 0.9.0",
"insta",
"memchr",
"ruff_annotate_snippets",
@@ -3320,7 +3317,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.8"
version = "0.11.5"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3444,11 +3441,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.21.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
dependencies = [
"boxcar",
"compact_str",
"compact_str 0.8.1",
"crossbeam-queue",
"dashmap 6.1.0",
"hashbrown 0.15.2",
@@ -3467,18 +3464,18 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.21.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
[[package]]
name = "salsa-macros"
version = "0.21.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7#42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"synstructure",
]
@@ -3512,7 +3509,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3561,7 +3558,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3572,7 +3569,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3595,7 +3592,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3636,7 +3633,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3661,9 +3658,9 @@ dependencies = [
[[package]]
name = "shellexpand"
version = "3.1.1"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [
"dirs",
]
@@ -3767,7 +3764,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3783,9 +3780,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.101"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
@@ -3800,7 +3797,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3871,7 +3868,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3882,7 +3879,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"test-case-core",
]
@@ -3918,7 +3915,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3929,7 +3926,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4060,7 +4057,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4314,7 +4311,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
"js-sys",
"rand 0.9.1",
"rand 0.9.0",
"uuid-macro-internal",
"wasm-bindgen",
]
@@ -4327,7 +4324,7 @@ checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4449,7 +4446,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"wasm-bindgen-shared",
]
@@ -4484,7 +4481,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4519,7 +4516,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4585,7 +4582,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -4634,7 +4631,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4645,7 +4642,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4883,7 +4880,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"synstructure",
]
@@ -4904,7 +4901,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4924,7 +4921,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"synstructure",
]
@@ -4947,7 +4944,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]

View File

@@ -55,6 +55,7 @@ bitflags = { version = "2.5.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
camino = { version = "1.1.7" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
@@ -94,7 +95,6 @@ insta-cmd = { version = "0.6.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.14.0" }
jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
libc = { version = "0.2.153" }
@@ -124,7 +124,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 = "42f15835c0005c4b37aaf5bc1a15e3e1b3df14b7" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -272,9 +272,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.4"
# Make distability of apps opt-in instead of opt-out
dist = false
cargo-dist-version = "0.28.4-prerelease.1"
# CI backends to support
ci = "github"
# The installers to generate for each app
@@ -308,7 +306,7 @@ auto-includes = false
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "plan"
pr-run-mode = "skip"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase dist should use to create the GitHub release
@@ -336,7 +334,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
global = "depot-ubuntu-latest-4"
[workspace.metadata.dist.github-action-commits]
"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0
"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4
"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2
"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3

View File

@@ -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.8/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.8/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.5/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.8
rev: v0.11.5
hooks:
# Run the linter.
- id: ruff

View File

@@ -20,12 +20,12 @@ ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }
colored = { workspace = true }
countme = { workspace = true, features = ["enable"] }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
jiff = { workspace = true }
rayon = { workspace = true }
salsa = { workspace = true }
tracing = { workspace = true, features = ["release_max_level_debug"] }

View File

@@ -2,16 +2,16 @@
## Basics
`mypy_primer` can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
```sh
uvx --from "git+https://github.com/hauntsaninja/mypy_primer" mypy_primer -h
uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h
```
Alternatively, you can install the forked version of `mypy_primer` using:
```sh
uv tool install "git+https://github.com/hauntsaninja/mypy_primer"
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
```
and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]).
@@ -31,7 +31,7 @@ mypy_primer \
```
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt))\$"`.
diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`.
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration
option to work correctly.
@@ -56,5 +56,6 @@ mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch …
Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly.
[full list of ecosystem projects]: https://github.com/hauntsaninja/mypy_primer/blob/master/mypy_primer/projects.py
[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support
[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py
[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables

View File

@@ -105,19 +105,6 @@ pub(crate) struct CheckCommand {
/// Watch files for changes and recheck files related to the changed files.
#[arg(long, short = 'W')]
pub(crate) watch: bool,
/// Respect file exclusions via `.gitignore` and other standard ignore files.
/// Use `--no-respect-gitignore` to disable.
#[arg(
long,
overrides_with("no_respect_ignore_files"),
help_heading = "File selection",
default_missing_value = "true",
num_args = 0..1
)]
respect_ignore_files: Option<bool>,
#[clap(long, overrides_with("respect_ignore_files"), hide = true)]
no_respect_ignore_files: bool,
}
impl CheckCommand {
@@ -133,13 +120,6 @@ impl CheckCommand {
)
};
// --no-respect-gitignore defaults to false and is set true by CLI flag. If passed, override config file
// Otherwise, only pass this through if explicitly set (don't default to anything here to
// make sure that doesn't take precedence over an explicitly-set config file value)
let respect_ignore_files = self
.no_respect_ignore_files
.then_some(false)
.or(self.respect_ignore_files);
Options {
environment: Some(EnvironmentOptions {
python_version: self
@@ -164,7 +144,6 @@ impl CheckCommand {
error_on_warning: self.error_on_warning,
}),
rules,
respect_ignore_files,
..Default::default()
}
}

View File

@@ -190,8 +190,8 @@ where
let ansi = writer.has_ansi_escapes();
if self.display_timestamp {
let timestamp = jiff::Zoned::now()
.strftime("%Y-%m-%d %H:%M:%S.%f")
let timestamp = chrono::Local::now()
.format("%Y-%m-%d %H:%M:%S.%f")
.to_string();
if ansi {
write!(writer, "{} ", timestamp.dimmed())?;
@@ -199,7 +199,7 @@ where
write!(
writer,
"{} ",
jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S.%f")
chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f")
)?;
}
}

View File

@@ -169,12 +169,8 @@ pub enum ExitStatus {
/// Checking was successful but there were errors.
Failure = 1,
/// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...)
/// Checking failed.
Error = 2,
/// Internal Red Knot error (panic, or any other error that isn't due to the user using the
/// program incorrectly or transient environment errors).
InternalError = 101,
}
impl Termination for ExitStatus {
@@ -250,16 +246,11 @@ impl MainLoop {
// Spawn a new task that checks the project. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || {
match db.check() {
Ok(result) => {
// Send the result back to the main loop for printing.
sender
.send(MainLoopMessage::CheckCompleted { result, revision })
.unwrap();
}
Err(cancelled) => {
tracing::debug!("Check has been cancelled: {cancelled:?}");
}
if let Ok(result) = db.check() {
// Send the result back to the main loop for printing.
sender
.send(MainLoopMessage::CheckCompleted { result, revision })
.unwrap();
}
});
}
@@ -273,6 +264,12 @@ impl MainLoop {
.format(terminal_settings.output_format)
.color(colored::control::SHOULD_COLORIZE.should_colorize());
let min_error_severity = if terminal_settings.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
if check_revision == revision {
if db.project().files(db).is_empty() {
tracing::warn!("No python files found under the given path(s)");
@@ -287,13 +284,13 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
} else {
let mut max_severity = Severity::Info;
let mut failed = false;
let diagnostics_count = result.len();
for diagnostic in result {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
max_severity = max_severity.max(diagnostic.severity());
failed |= diagnostic.severity() >= min_error_severity;
}
writeln!(
@@ -304,17 +301,10 @@ impl MainLoop {
)?;
if self.watcher.is_none() {
return Ok(match max_severity {
Severity::Info => ExitStatus::Success,
Severity::Warning => {
if terminal_settings.error_on_warning {
ExitStatus::Failure
} else {
ExitStatus::Success
}
}
Severity::Error => ExitStatus::Failure,
Severity::Fatal => ExitStatus::InternalError,
return Ok(if failed {
ExitStatus::Failure
} else {
ExitStatus::Success
});
}
}

View File

@@ -3,10 +3,6 @@
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum PythonVersion {
#[value(name = "3.7")]
Py37,
#[value(name = "3.8")]
Py38,
#[default]
#[value(name = "3.9")]
Py39,
@@ -23,8 +19,6 @@ pub enum PythonVersion {
impl PythonVersion {
const fn as_str(self) -> &'static str {
match self {
Self::Py37 => "3.7",
Self::Py38 => "3.8",
Self::Py39 => "3.9",
Self::Py310 => "3.10",
Self::Py311 => "3.11",
@@ -43,8 +37,6 @@ impl std::fmt::Display for PythonVersion {
impl From<PythonVersion> for ruff_python_ast::PythonVersion {
fn from(value: PythonVersion) -> Self {
match value {
PythonVersion::Py37 => Self::PY37,
PythonVersion::Py38 => Self::PY38,
PythonVersion::Py39 => Self::PY39,
PythonVersion::Py310 => Self::PY310,
PythonVersion::Py311 => Self::PY311,

View File

@@ -5,116 +5,6 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = TestCase::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> <temp_dir>/test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
let case = TestCase::with_files([(".test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> .test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_respect_ignore_files() -> anyhow::Result<()> {
// First test that the default option works correctly (the file is skipped)
let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN No python files found under the given path(s)
");
// Test that we can set to false via CLI
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
// Test that we can set to false via config file
case.write_file("knot.toml", "respect-ignore-files = false")?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
// Ensure CLI takes precedence
case.write_file("knot.toml", "respect-ignore-files = true")?;
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
/// Specifying an option on the CLI should take precedence over the same setting in the
/// project's configuration. Here, this is tested for the Python version.
#[test]
@@ -142,12 +32,12 @@ fn config_override_python_version() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-attribute: Type `<module 'sys'>` has no attribute `last_exc`
--> test.py:5:7
error: lint:unresolved-attribute
--> <temp_dir>/test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
|
Found 1 diagnostic
@@ -194,7 +84,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> test.py:5:1
--> <temp_dir>/test.py:5:1
|
3 | from typing_extensions import reveal_type
4 |
@@ -212,7 +102,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> test.py:5:1
--> <temp_dir>/test.py:5:1
|
3 | from typing_extensions import reveal_type
4 |
@@ -275,11 +165,11 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import: Cannot resolve import `utils`
--> test.py:2:6
error: lint:unresolved-import
--> <temp_dir>/child/test.py:2:6
|
2 | from utils import add
| ^^^^^
| ^^^^^ Cannot resolve import `utils`
3 |
4 | stat = add(10, 15)
|
@@ -362,7 +252,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
r#"
y = 4 / 0
for a in range(0, int(y)):
for a in range(0, y):
x = a
print(x) # possibly-unresolved-reference
@@ -375,22 +265,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
error: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, int(y)):
4 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:7:7
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -411,13 +301,13 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
warning: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, int(y)):
4 | for a in range(0, y):
|
Found 1 diagnostic
@@ -438,7 +328,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
y = 4 / 0
for a in range(0, int(y)):
for a in range(0, y):
x = a
print(x) # possibly-unresolved-reference
@@ -451,33 +341,33 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> test.py:2:8
error: lint:unresolved-import
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
3 |
4 | y = 4 / 0
|
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
error: lint:division-by-zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, int(y)):
6 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:9:7
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:9:7
|
7 | x = a
8 |
9 | print(x) # possibly-unresolved-reference
| ^
| ^ Name `x` used when possibly not defined
|
Found 3 diagnostics
@@ -498,24 +388,24 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> test.py:2:8
warning: lint:unresolved-import
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
3 |
4 | y = 4 / 0
|
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
warning: lint:division-by-zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, int(y)):
6 | for a in range(0, y):
|
Found 2 diagnostics
@@ -536,7 +426,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
r#"
y = 4 / 0
for a in range(0, int(y)):
for a in range(0, y):
x = a
print(x) # possibly-unresolved-reference
@@ -549,22 +439,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
error: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, int(y)):
4 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:7:7
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -586,13 +476,13 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
warning: lint:division-by-zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, int(y)):
4 | for a in range(0, y):
|
Found 1 diagnostic
@@ -623,7 +513,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
warning: unknown-rule
--> pyproject.toml:3:1
--> <temp_dir>/pyproject.toml:3:1
|
2 | [tool.knot.rules]
3 | division-by-zer = "warn" # incorrect rule name
@@ -665,11 +555,11 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
|
Found 1 diagnostic
@@ -695,7 +585,7 @@ fn exit_code_only_info() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> test.py:3:1
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
@@ -725,7 +615,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> test.py:3:1
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
@@ -748,11 +638,11 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
|
Found 1 diagnostic
@@ -780,11 +670,11 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
|
Found 1 diagnostic
@@ -809,20 +699,20 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:2:7
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
Found 2 diagnostics
@@ -847,20 +737,20 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:2:7
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
Found 2 diagnostics
@@ -885,20 +775,20 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:2:7
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
Found 2 diagnostics
@@ -924,7 +814,7 @@ fn user_configuration() -> anyhow::Result<()> {
r#"
y = 4 / 0
for a in range(0, int(y)):
for a in range(0, y):
x = a
print(x)
@@ -945,22 +835,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, int(y)):
4 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> main.py:7:7
warning: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -987,22 +877,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, int(y)):
4 | for a in range(0, y):
|
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> main.py:7:7
error: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^
| ^ Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -1045,27 +935,27 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^
|
error: lint:unresolved-import: Cannot resolve import `main2`
--> project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^
3 |
4 | print(z)
|
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> project/tests/test_main.py:2:8
error: lint:unresolved-import
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
error: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
error: lint:unresolved-import
--> <temp_dir>/project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
3 |
4 | print(z)
|
Found 3 diagnostics
@@ -1082,20 +972,20 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import: Cannot resolve import `main2`
--> project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^
3 |
4 | print(z)
|
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> project/tests/test_main.py:2:8
error: lint:unresolved-import
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
error: lint:unresolved-import
--> <temp_dir>/project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
3 |
4 | print(z)
|
Found 2 diagnostics
@@ -1152,8 +1042,8 @@ fn concise_diagnostics() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning[lint:unresolved-reference] test.py:2:7: Name `x` used when not defined
error[lint:non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
warning[lint:unresolved-reference] <temp_dir>/test.py:2:7: Name `x` used when not defined
error[lint:non-subscriptable] <temp_dir>/test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
Found 2 diagnostics
----- stderr -----
@@ -1186,7 +1076,7 @@ fn concise_revealed_type() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
info[revealed-type] test.py:5:1: Revealed type: `Literal["hello"]`
info[revealed-type] <temp_dir>/test.py:5:1: Revealed type: `Literal["hello"]`
Found 1 diagnostic
----- stderr -----

View File

@@ -1,39 +0,0 @@
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{AnyNodeRef, Identifier};
use ruff_text_size::TextSize;
use crate::Db;
pub struct Completion {
pub label: String,
}
pub fn completion(db: &dyn Db, file: File, _offset: TextSize) -> Vec<Completion> {
let parsed = parsed_module(db.upcast(), file);
identifiers(parsed.syntax().into())
.into_iter()
.map(|label| Completion { label })
.collect()
}
fn identifiers(node: AnyNodeRef) -> Vec<String> {
struct Visitor {
identifiers: Vec<String>,
}
impl<'a> SourceOrderVisitor<'a> for Visitor {
fn visit_identifier(&mut self, id: &'a Identifier) {
self.identifiers.push(id.id.as_str().to_string());
}
}
let mut visitor = Visitor {
identifiers: vec![],
};
node.visit_source_order(&mut visitor);
visitor.identifiers.sort();
visitor.identifiers.dedup();
visitor.identifiers
}

View File

@@ -10,7 +10,7 @@ pub(crate) mod tests {
use super::Db;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
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;
@@ -83,10 +83,6 @@ pub(crate) mod tests {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
}
impl Upcast<dyn SourceDb> for TestDb {

View File

@@ -272,9 +272,9 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> main.py:2:19
--> /main.py:2:19
|
2 | class Test: ...
| ^^^^
@@ -282,14 +282,14 @@ mod tests {
4 | ab = Test()
|
info: Source
--> main.py:4:13
--> /main.py:4:13
|
2 | class Test: ...
3 |
4 | ab = Test()
| ^^
|
");
"###);
}
#[test]
@@ -304,9 +304,9 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> main.py:2:17
--> /main.py:2:17
|
2 | def foo(a, b): ...
| ^^^
@@ -314,14 +314,14 @@ mod tests {
4 | ab = foo
|
info: Source
--> main.py:6:13
--> /main.py:6:13
|
4 | ab = foo
5 |
6 | ab
| ^^
|
");
"###);
}
#[test]
@@ -344,7 +344,7 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> main.py:3:17
--> /main.py:3:17
|
3 | def foo(a, b): ...
| ^^^
@@ -352,7 +352,7 @@ mod tests {
5 | def bar(a, b): ...
|
info: Source
--> main.py:12:13
--> /main.py:12:13
|
10 | a = bar
11 |
@@ -361,7 +361,7 @@ mod tests {
|
info: lint:goto-type-definition: Type definition
--> main.py:5:17
--> /main.py:5:17
|
3 | def foo(a, b): ...
4 |
@@ -371,7 +371,7 @@ mod tests {
7 | if random.choice():
|
info: Source
--> main.py:12:13
--> /main.py:12:13
|
10 | a = bar
11 |
@@ -395,13 +395,13 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> lib.py:1:1
--> /lib.py:1:1
|
1 | a = 10
| ^^^^^^
|
info: Source
--> main.py:4:13
--> /main.py:4:13
|
2 | import lib
3 |
@@ -433,7 +433,7 @@ mod tests {
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> main.py:4:13
--> /main.py:4:13
|
2 | a: str = "test"
3 |
@@ -462,7 +462,7 @@ mod tests {
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> main.py:2:22
--> /main.py:2:22
|
2 | a: str = "test"
| ^^^^^^
@@ -478,20 +478,20 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> main.py:2:24
--> /main.py:2:24
|
2 | type Alias[T: int = bool] = list[T]
| ^
|
info: Source
--> main.py:2:46
--> /main.py:2:46
|
2 | type Alias[T: int = bool] = list[T]
| ^
|
");
"###);
}
#[test]
@@ -544,7 +544,7 @@ mod tests {
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> main.py:4:18
--> /main.py:4:18
|
2 | def test(a: str): ...
3 |
@@ -579,7 +579,7 @@ mod tests {
233 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
info: Source
--> main.py:4:18
--> /main.py:4:18
|
2 | def test(a: str): ...
3 |
@@ -613,7 +613,7 @@ f(**kwargs<CURSOR>)
1088 | # Also multiprocessing.managers.SyncManager.dict()
|
info: Source
--> main.py:6:5
--> /main.py:6:5
|
4 | kwargs = { "name": "test"}
5 |
@@ -644,7 +644,7 @@ f(**kwargs<CURSOR>)
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> main.py:3:17
--> /main.py:3:17
|
2 | def foo(a: str):
3 | a
@@ -666,23 +666,23 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> main.py:2:19
--> /main.py:2:19
|
2 | class X:
| ^
3 | def foo(a, b): ...
|
info: Source
--> main.py:7:13
--> /main.py:7:13
|
5 | x = X()
6 |
7 | x.foo()
| ^
|
");
"###);
}
#[test]
@@ -695,9 +695,9 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
assert_snapshot!(test.goto_type_definition(), @r###"
info: lint:goto-type-definition: Type definition
--> main.py:2:17
--> /main.py:2:17
|
2 | def foo(a, b): ...
| ^^^
@@ -705,14 +705,14 @@ f(**kwargs<CURSOR>)
4 | foo()
|
info: Source
--> main.py:4:13
--> /main.py:4:13
|
2 | def foo(a, b): ...
3 |
4 | foo()
| ^^^
|
");
"###);
}
#[test]
@@ -737,7 +737,7 @@ f(**kwargs<CURSOR>)
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> main.py:4:27
--> /main.py:4:27
|
2 | def foo(a: str | None, b):
3 | if a is not None:
@@ -767,7 +767,7 @@ f(**kwargs<CURSOR>)
672 | def __bool__(self) -> Literal[False]: ...
|
info: Source
--> main.py:3:17
--> /main.py:3:17
|
2 | def foo(a: str | None, b):
3 | a
@@ -785,7 +785,7 @@ f(**kwargs<CURSOR>)
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> main.py:3:17
--> /main.py:3:17
|
2 | def foo(a: str | None, b):
3 | a

View File

@@ -156,7 +156,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:4:9
--> /main.py:4:9
|
2 | a = 10
3 |
@@ -192,7 +192,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:10:9
--> /main.py:10:9
|
9 | foo = Foo()
10 | foo.a
@@ -214,7 +214,7 @@ mod tests {
"#,
);
assert_snapshot!(test.hover(), @r"
assert_snapshot!(test.hover(), @r###"
def foo(a, b) -> Unknown
---------------------------------------------
```text
@@ -222,7 +222,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:4:13
--> /main.py:4:13
|
2 | def foo(a, b): ...
3 |
@@ -231,7 +231,7 @@ mod tests {
| |
| source
|
");
"###);
}
#[test]
@@ -251,7 +251,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:3:17
--> /main.py:3:17
|
2 | def foo(a: int, b: int, c: int):
3 | a + b == c
@@ -282,7 +282,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:4:18
--> /main.py:4:18
|
2 | def test(a: int): ...
3 |
@@ -312,7 +312,7 @@ mod tests {
"#,
);
assert_snapshot!(test.hover(), @r"
assert_snapshot!(test.hover(), @r###"
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
---------------------------------------------
```text
@@ -320,7 +320,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:12:13
--> /main.py:12:13
|
10 | a = bar
11 |
@@ -329,7 +329,7 @@ mod tests {
| |
| source
|
");
"###);
}
#[test]
@@ -352,7 +352,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:4:13
--> /main.py:4:13
|
2 | import lib
3 |
@@ -381,7 +381,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:2:46
--> /main.py:2:46
|
2 | type Alias[T: int = bool] = list[T]
| ^- Cursor offset
@@ -407,7 +407,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:2:53
--> /main.py:2:53
|
2 | type Alias[**P = [int, str]] = Callable[P, int]
| ^- Cursor offset
@@ -433,7 +433,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:2:43
--> /main.py:2:43
|
2 | type Alias[*Ts = ()] = tuple[*Ts]
| ^^- Cursor offset
@@ -444,129 +444,7 @@ mod tests {
}
#[test]
fn hover_variable_assignment() {
let test = cursor_test(
r#"
value<CURSOR> = 1
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
```text
Literal[1]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:2:13
|
2 | value = 1
| ^^^^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_augmented_assignment() {
let test = cursor_test(
r#"
value = 1
value<CURSOR> += 2
"#,
);
// We currently show the *previous* value of the variable (1), not the new one (3).
// Showing the new value might be more intuitive for some users, but the actual 'use'
// of the `value` symbol here in read-context is `1`. This comment mainly exists to
// signal that it might be okay to revisit this in the future and reveal 3 instead.
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
```text
Literal[1]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:3:13
|
2 | value = 1
3 | value += 2
| ^^^^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_attribute_assignment() {
let test = cursor_test(
r#"
class C:
attr: int = 1
C.attr<CURSOR> = 2
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[2]
---------------------------------------------
```text
Literal[2]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:5:13
|
3 | attr: int = 1
4 |
5 | C.attr = 2
| ^^^^^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_augmented_attribute_assignment() {
let test = cursor_test(
r#"
class C:
attr = 1
C.attr<CURSOR> += 2
"#,
);
// See the comment in the `hover_augmented_assignment` test above. The same
// reasoning applies here.
assert_snapshot!(test.hover(), @r"
Unknown | Literal[1]
---------------------------------------------
```text
Unknown | Literal[1]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:5:13
|
3 | attr = 1
4 |
5 | C.attr += 2
| ^^^^^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_annotated_assignment() {
fn hover_class_member_declaration() {
let test = cursor_test(
r#"
class Foo:
@@ -574,15 +452,16 @@ mod tests {
"#,
);
// TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122
assert_snapshot!(test.hover(), @r"
int
Never
---------------------------------------------
```text
int
Never
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:3:13
--> /main.py:3:13
|
2 | class Foo:
3 | a: int
@@ -593,64 +472,6 @@ mod tests {
");
}
#[test]
fn hover_annotated_assignment_with_rhs() {
let test = cursor_test(
r#"
class Foo:
a<CURSOR>: int = 1
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
```text
Literal[1]
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:3:13
|
2 | class Foo:
3 | a: int = 1
| ^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_annotated_attribute_assignment() {
let test = cursor_test(
r#"
class Foo:
def __init__(self, a: int):
self.a<CURSOR>: int = a
"#,
);
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
```text
int
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:4:17
|
2 | class Foo:
3 | def __init__(self, a: int):
4 | self.a: int = a
| ^^^^^^- Cursor offset
| |
| source
|
");
}
#[test]
fn hover_type_narrowing() {
let test = cursor_test(
@@ -669,7 +490,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> main.py:4:27
--> /main.py:4:27
|
2 | def foo(a: str | None, b):
3 | if a is not None:

View File

@@ -1,4 +1,3 @@
mod completion;
mod db;
mod find_node;
mod goto;
@@ -6,7 +5,6 @@ mod hover;
mod inlay_hints;
mod markup;
pub use completion::completion;
pub use db::Db;
pub use goto::goto_type_definition;
pub use hover::hover;

View File

@@ -1,9 +0,0 @@
# Regression test for an issue that came up while working
# on https://github.com/astral-sh/ruff/pull/17769
class C:
def method[T](self, x: T) -> T:
def inner():
self.attr = 1
C().attr

View File

@@ -1 +0,0 @@
t: "tuple[list[int]]"

View File

@@ -1,15 +0,0 @@
# Regression test for https://github.com/astral-sh/ruff/issues/17215
# panicked in commit 1a6a10b30
# error message:
# dependency graph cycle querying all_narrowing_constraints_for_expression(Id(8591))
def f(a: A, b: B, c: C):
unknown_a: UA = make_unknown()
unknown_b: UB = make_unknown()
unknown_c: UC = make_unknown()
unknown_d: UD = make_unknown()
if unknown_a and unknown_b:
if unknown_c:
if unknown_d:
return a, b, c

View File

@@ -1,22 +0,0 @@
# Regression test for https://github.com/astral-sh/ruff/issues/17215
# panicked in commit 1a6a10b30
# error message:
# dependency graph cycle querying all_negative_narrowing_constraints_for_expression(Id(859f))
def f(f1: bool, f2: bool, f3: bool, f4: bool):
o1: UnknownClass = make_o()
o2: UnknownClass = make_o()
o3: UnknownClass = make_o()
o4: UnknownClass = make_o()
if f1 and f2 and f3 and f4:
if o1 == o2:
return None
if o2 == o3:
return None
if o3 == o4:
return None
if o4 == o1:
return None
return o1, o2, o3, o4

View File

@@ -1,4 +0,0 @@
from __future__ import annotations
def foo(a: foo()):
pass

View File

@@ -149,10 +149,6 @@ impl SourceDb for ProjectDatabase {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
}
#[salsa::db]
@@ -211,7 +207,7 @@ pub(crate) mod tests {
use salsa::Event;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{Db as SemanticDb, Program};
use red_knot_python_semantic::Db as SemanticDb;
use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
@@ -285,10 +281,6 @@ pub(crate) mod tests {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
}
impl Upcast<dyn SemanticDb> for TestDb {

View File

@@ -10,8 +10,7 @@ use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSele
use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
DiagnosticId, Severity, Span, SubDiagnostic,
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span,
};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
@@ -20,11 +19,8 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
use rustc_hash::FxHashSet;
use salsa::Durability;
use salsa::Setter;
use std::backtrace::BacktraceStatus;
use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::sync::Arc;
use thiserror::Error;
use tracing::error;
pub mod combine;
@@ -190,66 +186,30 @@ impl Project {
.map(IOErrorDiagnostic::to_diagnostic),
);
let file_diagnostics = Arc::new(std::sync::Mutex::new(vec![]));
let result = Arc::new(std::sync::Mutex::new(diagnostics));
let inner_result = Arc::clone(&result);
{
let file_diagnostics = Arc::clone(&file_diagnostics);
let db = db.clone();
let project_span = project_span.clone();
let db = db.clone();
let project_span = project_span.clone();
rayon::scope(move |scope| {
for file in &files {
let result = Arc::clone(&file_diagnostics);
let db = db.clone();
let project_span = project_span.clone();
rayon::scope(move |scope| {
for file in &files {
let result = inner_result.clone();
let db = db.clone();
let project_span = project_span.clone();
scope.spawn(move |_| {
let check_file_span =
tracing::debug_span!(parent: &project_span, "check_file", ?file);
let _entered = check_file_span.entered();
scope.spawn(move |_| {
let check_file_span =
tracing::debug_span!(parent: &project_span, "check_file", ?file);
let _entered = check_file_span.entered();
let file_diagnostics = check_file_impl(&db, file);
result.lock().unwrap().extend(file_diagnostics);
});
}
});
}
let mut file_diagnostics = Arc::into_inner(file_diagnostics)
.unwrap()
.into_inner()
.unwrap();
// We sort diagnostics in a way that keeps them in source order
// and grouped by file. After that, we fall back to severity
// (with fatal messages sorting before info messages) and then
// finally the diagnostic ID.
file_diagnostics.sort_by(|d1, d2| {
if let (Some(span1), Some(span2)) = (d1.primary_span(), d2.primary_span()) {
let order = span1
.file()
.path(db)
.as_str()
.cmp(span2.file().path(db).as_str());
if order.is_ne() {
return order;
}
if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
let order = range1.start().cmp(&range2.start());
if order.is_ne() {
return order;
}
}
let file_diagnostics = check_file_impl(&db, file);
result.lock().unwrap().extend(file_diagnostics);
});
}
// Reverse so that, e.g., Fatal sorts before Info.
let order = d1.severity().cmp(&d2.severity()).reverse();
if order.is_ne() {
return order;
}
d1.id().cmp(&d2.id())
});
diagnostics.extend(file_diagnostics);
diagnostics
Arc::into_inner(result).unwrap().into_inner().unwrap()
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
@@ -353,23 +313,20 @@ impl Project {
/// * It has a [`SystemPath`] and belongs to a package's `src` files
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath)
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool {
let path = file.path(db);
// Try to return early to avoid adding a dependency on `open_files` or `file_set` which
// both have a durability of `LOW`.
if path.is_vendored_path() {
return false;
}
if let Some(open_files) = self.open_files(db) {
open_files.contains(&file)
} else if file.path(db).is_system_path() {
self.files(db).contains(&file)
self.contains_file(db, file)
} else {
file.path(db).is_system_virtual_path()
}
}
/// Returns `true` if `file` is a first-party file part of this package.
pub fn contains_file(self, db: &dyn Db, file: File) -> bool {
self.files(db).contains(&file)
}
#[tracing::instrument(level = "debug", skip(self, db))]
pub fn remove_file(self, db: &mut dyn Db, file: File) {
tracing::debug!(
@@ -467,23 +424,7 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
.map(|error| create_parse_diagnostic(file, error)),
);
diagnostics.extend(
parsed
.unsupported_syntax_errors()
.iter()
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
);
{
let db = AssertUnwindSafe(db);
match catch(&**db, file, || check_types(db.upcast(), file)) {
Ok(Some(type_check_diagnostics)) => {
diagnostics.extend(type_check_diagnostics.into_iter().cloned());
}
Ok(None) => {}
Err(diagnostic) => diagnostics.push(diagnostic),
}
}
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
@@ -574,99 +515,16 @@ enum IOErrorKind {
SourceText(#[from] SourceTextError),
}
fn catch<F, R>(db: &dyn Db, file: File, f: F) -> Result<Option<R>, Diagnostic>
where
F: FnOnce() -> R + UnwindSafe,
{
match ruff_db::panic::catch_unwind(|| {
// Ignore salsa errors
salsa::Cancelled::catch(f).ok()
}) {
Ok(result) => Ok(result),
Err(error) => {
use std::fmt::Write;
let mut message = String::new();
message.push_str("Panicked");
if let Some(location) = error.location {
let _ = write!(&mut message, " at {location}");
}
let _ = write!(
&mut message,
" when checking `{file}`",
file = file.path(db)
);
if let Some(payload) = error.payload.as_str() {
let _ = write!(&mut message, ": `{payload}`");
}
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
"This indicates a bug in Red Knot.",
));
let report_message = "If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bred-knot%5D:%20panic we'd be very appreciative!";
diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message));
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
format!(
"Platform: {os} {arch}",
os = std::env::consts::OS,
arch = std::env::consts::ARCH
),
));
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
format!(
"Args: {args:?}",
args = std::env::args().collect::<Vec<_>>()
),
));
if let Some(backtrace) = error.backtrace {
match backtrace.status() {
BacktraceStatus::Disabled => {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
"run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information",
));
}
BacktraceStatus::Captured => {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
format!("Backtrace:\n{backtrace}"),
));
}
_ => {}
}
}
if let Some(backtrace) = error.salsa_backtrace {
salsa::attach(db, || {
diagnostic.sub(SubDiagnostic::new(Severity::Info, backtrace.to_string()));
});
}
Err(diagnostic)
}
}
}
#[cfg(test)]
mod tests {
use crate::db::tests::TestDb;
use crate::{check_file_impl, ProjectMetadata};
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ruff_db::files::system_path_to_file;
use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name;
use ruff_python_ast::PythonVersion;
#[test]
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
@@ -674,16 +532,6 @@ mod tests {
let mut db = TestDb::new(project);
let path = SystemPath::new("test.py");
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},
)
.expect("Failed to configure program settings");
db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap();

View File

@@ -32,9 +32,6 @@ pub struct Options {
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub respect_ignore_files: Option<bool>,
}
impl Options {
@@ -68,7 +65,9 @@ impl Options {
.and_then(|env| env.python_platform.as_deref().cloned())
.unwrap_or_else(|| {
let default = PythonPlatform::default();
tracing::info!("Defaulting to python-platform `{default}`");
tracing::info!(
"Defaulting to default python version for this platform: '{default}'",
);
default
});
ProgramSettings {
@@ -134,7 +133,7 @@ impl Options {
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
let (rules, diagnostics) = self.to_rule_selection(db);
let mut settings = Settings::new(rules, self.respect_ignore_files);
let mut settings = Settings::new(rules);
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {

View File

@@ -96,7 +96,10 @@ impl Project {
return Ok(None);
};
let minor = versions.next().copied().unwrap_or_default();
let mut minor = versions.next().copied().unwrap_or_default();
// Ensure minor is at least 9
minor = minor.max(9);
tracing::debug!("Resolved requires-python constraint to: {major}.{minor}");

View File

@@ -21,16 +21,13 @@ pub struct Settings {
rules: Arc<RuleSelection>,
terminal: TerminalSettings,
respect_ignore_files: bool,
}
impl Settings {
pub fn new(rules: RuleSelection, respect_ignore_files: Option<bool>) -> Self {
pub fn new(rules: RuleSelection) -> Self {
Self {
rules: Arc::new(rules),
terminal: TerminalSettings::default(),
respect_ignore_files: respect_ignore_files.unwrap_or(true),
}
}
@@ -38,10 +35,6 @@ impl Settings {
&self.rules
}
pub fn respect_ignore_files(&self) -> bool {
self.respect_ignore_files
}
pub fn to_rules(&self) -> Arc<RuleSelection> {
self.rules.clone()
}

View File

@@ -129,11 +129,7 @@ impl<'a> ProjectFilesWalker<'a> {
{
let mut paths = paths.into_iter();
let mut walker = db
.system()
.walk_directory(paths.next()?.as_ref())
.standard_filters(db.project().settings(db).respect_ignore_files())
.ignore_hidden(false);
let mut walker = db.system().walk_directory(paths.next()?.as_ref());
for path in paths {
walker = walker.add(path);

View File

@@ -6,9 +6,7 @@ use ruff_db::parsed::parsed_module;
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
use ruff_python_ast::visitor::source_order;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{
self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt,
};
use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt};
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
let project = ProjectMetadata::discover(project_root, &system)?;
@@ -260,14 +258,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
source_order::walk_expr(self, expr);
}
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
self.visit_expr(&comprehension.iter);
self.visit_target(&comprehension.target);
for if_expr in &comprehension.ifs {
self.visit_expr(if_expr);
}
}
fn visit_parameter(&mut self, parameter: &Parameter) {
let _ty = parameter.inferred_type(&self.model);

View File

@@ -20,12 +20,6 @@ from watchfiles import Change, watch
CRATE_NAME: Final = "red_knot_python_semantic"
CRATE_ROOT: Final = Path(__file__).resolve().parent
RED_KNOT_VENDORED: Final = CRATE_ROOT.parent / "red_knot_vendored"
DIRS_TO_WATCH: Final = (
CRATE_ROOT,
RED_KNOT_VENDORED,
CRATE_ROOT.parent / "red_knot_test/src",
)
MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest"
@@ -164,24 +158,20 @@ class MDTestRunner:
self._run_mdtest()
self.console.print("[dim]Ready to watch for changes...[/dim]")
for changes in watch(*DIRS_TO_WATCH):
for changes in watch(CRATE_ROOT):
new_md_files = set()
changed_md_files = set()
rust_code_has_changed = False
vendored_typeshed_has_changed = False
for change, path_str in changes:
path = Path(path_str)
match path.suffix:
case ".rs":
rust_code_has_changed = True
case ".pyi" if path.is_relative_to(RED_KNOT_VENDORED):
vendored_typeshed_has_changed = True
case ".md":
pass
case _:
continue
if path.suffix == ".rs":
rust_code_has_changed = True
continue
if path.suffix != ".md":
continue
try:
relative_path = Path(path).relative_to(MDTEST_DIR)
@@ -209,11 +199,6 @@ class MDTestRunner:
if rust_code_has_changed:
if self._recompile_tests("Rust code has changed, recompiling tests..."):
self._run_mdtest()
elif vendored_typeshed_has_changed:
if self._recompile_tests(
"Vendored typeshed has changed, recompiling tests..."
):
self._run_mdtest()
elif new_md_files:
files = " ".join(file.as_posix() for file in new_md_files)
self._recompile_tests(

View File

@@ -46,76 +46,28 @@ def f():
y: Any = "not an Any" # error: [invalid-assignment]
```
## Subclasses of `Any`
## Subclass
The spec allows you to define subclasses of `Any`.
`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
assignable to `int`.
```py
from typing import Any
class SubclassOfAny(Any): ...
class Subclass(Any): ...
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: SubclassOfAny = 1 # error: [invalid-assignment]
y: int = SubclassOfAny()
```
x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not
possibly be a subclass of `FinalClass`:
```py
from typing import final
@final
class FinalClass: ...
f: FinalClass = SubclassOfAny() # error: [invalid-assignment]
@final
class OtherFinalClass: ...
f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment]
```
A subclass of `Any` can also be assigned to arbitrary `Callable` types:
```py
from typing import Callable, Any
def takes_callable1(f: Callable):
f()
takes_callable1(SubclassOfAny())
def takes_callable2(f: Callable[[int], None]):
f(1)
takes_callable2(SubclassOfAny())
```
A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:
```py
from typing import Any, Literal
class MockAny(Any):
pass
x: Literal[1] = MockAny() # error: [invalid-assignment]
```
A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should
be assignable to (almost) any type:
```py
from unittest.mock import MagicMock
x: int = MagicMock()
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
## Invalid

View File

@@ -225,25 +225,18 @@ Using `Concatenate` as the first argument to `Callable`:
from typing_extensions import Callable, Concatenate
def _(c: Callable[Concatenate[int, str, ...], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
And, as one of the parameter types:
```py
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
## Using `typing.ParamSpec`
```toml
[environment]
python-version = "3.12"
```
Using a `ParamSpec` in a `Callable` annotation:
```py
@@ -278,8 +271,7 @@ from typing_extensions import Callable, TypeVarTuple
Ts = TypeVarTuple("Ts")
def _(c: Callable[[int, *Ts], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
And, using the legacy syntax using `Unpack`:
@@ -288,8 +280,7 @@ And, using the legacy syntax using `Unpack`:
from typing_extensions import Unpack
def _(c: Callable[[int, Unpack[Ts]], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
## Member lookup

View File

@@ -48,11 +48,6 @@ reveal_type(get_foo()) # revealed: Foo
## Deferred self-reference annotations in a class definition
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
@@ -99,11 +94,6 @@ class Foo:
## Non-deferred self-reference annotations in a class definition
```toml
[environment]
python-version = "3.12"
```
```py
class Foo:
# error: [unresolved-reference]
@@ -156,24 +146,3 @@ def _():
def f(self) -> C:
return self
```
## Base class references
### Not deferred by __future__.annotations
```py
from __future__ import annotations
class A(B): # error: [unresolved-reference]
pass
class B:
pass
```
### Deferred in stub files
```pyi
class A(B): ...
class B: ...
```

View File

@@ -56,41 +56,40 @@ def _(
def bar() -> None:
return None
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(generics)
```
## Invalid Collection based AST nodes

View File

@@ -68,7 +68,7 @@ def x(
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, 5, "foo"] | None
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
@@ -108,7 +108,7 @@ def union_example(
None,
],
):
reveal_type(x) # revealed: Unknown | Literal[-1, 0, 1, "A", "B", "foo", "bar", b"A", b"\x00", b"\x07", True] | None
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
```
## Detecting Literal outside typing and typing_extensions
@@ -137,7 +137,7 @@ from other import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo(unknown type subscript)
reveal_type(a1) # revealed: @Todo(generics)
```
## Detecting typing_extensions.Literal

View File

@@ -38,12 +38,8 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
```py
from typing_extensions import LiteralString
# error: [invalid-type-form]
a: LiteralString[str]
# error: [invalid-type-form]
# error: [unresolved-reference] "Name `foo` used when not defined"
b: LiteralString["foo"]
a: LiteralString[str] # error: [invalid-type-form]
b: LiteralString["foo"] # error: [invalid-type-form]
```
### As a base class
@@ -76,11 +72,13 @@ reveal_type(baz) # revealed: Literal["bazfoo"]
qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
reveal_type(foo.join(qux)) # revealed: LiteralString
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function)
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
reveal_type(template.format(foo, bar)) # revealed: LiteralString
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function)
```
### Assignability

View File

@@ -1,10 +1,5 @@
# Starred expression annotations
```toml
[environment]
python-version = "3.11"
```
Type annotations for `*args` can be starred expressions themselves:
```py

View File

@@ -67,24 +67,21 @@ import typing
####################
### Built-ins
####################
class ListSubclass(typing.List): ...
# TODO: generic protocols
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: generic protocols
# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# TODO: generic protocols
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
@@ -95,35 +92,33 @@ reveal_type(FrozenSetSubclass.__mro__)
####################
### `collections`
####################
class ChainMapSubclass(typing.ChainMap): ...
# TODO: generic protocols
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]]
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# TODO: generic protocols
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]]
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -105,7 +105,7 @@ def f1(
from typing import Literal
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
```
## Class variables

View File

@@ -41,7 +41,7 @@ class Foo:
One thing that is supported is error messages for using special forms in type expressions.
```py
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec
def _(
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
@@ -49,7 +49,6 @@ def _(
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
e: ParamSpec,
f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions"
) -> None:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -66,7 +65,7 @@ You can't inherit from most of these. `typing.Callable` is an exception.
```py
from typing import Callable
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
class A(Self): ... # error: [invalid-base]
class B(Unpack): ... # error: [invalid-base]
@@ -74,27 +73,18 @@ class C(TypeGuard): ... # error: [invalid-base]
class D(TypeIs): ... # error: [invalid-base]
class E(Concatenate): ... # error: [invalid-base]
class F(Callable): ...
class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`"
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
```
## Subscriptability
```toml
[environment]
python-version = "3.12"
```
Some of these are not subscriptable:
```py
from typing_extensions import Self, TypeAlias, TypeVar
from typing_extensions import Self, TypeAlias
T = TypeVar("T")
# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
X: TypeAlias[T] = int
X: TypeAlias[T] = int # error: [invalid-type-form]
class Foo[T]:
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"

View File

@@ -11,6 +11,8 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict
X: Final = 42
Y: Final[int] = 42
# TODO: `TypedDict` is actually valid as a base
# error: [invalid-base]
class Bar(TypedDict):
x: Required[int]
y: NotRequired[str]

View File

@@ -25,11 +25,6 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
## Tuple annotations are understood
```toml
[environment]
python-version = "3.12"
```
`module.py`:
```py
@@ -61,7 +56,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
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(specialized non-generic class), @Todo(specialized non-generic class)]
reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)]
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]

View File

@@ -302,7 +302,7 @@ class C:
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b) # revealed: Unknown
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
```
#### Attributes defined in for-loop (unpacking)
@@ -397,27 +397,15 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class TupleIterator:
def __next__(self) -> tuple[int, str]:
return (1, "a")
class TupleIterable:
def __iter__(self) -> TupleIterator:
return TupleIterator()
class C:
def __init__(self) -> None:
[... for self.a in IntIterable()]
[... for (self.b, self.c) in TupleIterable()]
[... for self.d in IntIterable() for self.e in IntIterable()]
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | int
reveal_type(c_instance.b) # revealed: Unknown | int
reveal_type(c_instance.c) # revealed: Unknown | str
reveal_type(c_instance.d) # revealed: Unknown | int
reveal_type(c_instance.e) # revealed: Unknown | int
# TODO: Should be `Unknown | int`
# error: [unresolved-attribute]
reveal_type(c_instance.a) # revealed: Unknown
```
#### Conditionally declared / bound attributes
@@ -1677,7 +1665,7 @@ functions are instances of that class:
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
```
Some attributes are special-cased, however:
@@ -1710,9 +1698,9 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
bools are instances of that class:
```py
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
# revealed: bound method Literal[True].__and__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function)
reveal_type(True.__and__)
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
# revealed: bound method Literal[False].__or__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function)
reveal_type(False.__or__)
```
@@ -1728,8 +1716,7 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes
reveal_type(b"foo".join)
reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(generics), /) -> bytes
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
reveal_type(b"foo".endswith)
```
@@ -1832,89 +1819,6 @@ def f(never: Never):
never.another_attribute = never
```
### Cyclic implicit attributes
Inferring types for undeclared implicit attributes can be cyclic:
```py
class C:
def __init__(self):
self.x = 1
def copy(self, other: "C"):
self.x = other.x
reveal_type(C().x) # revealed: Unknown | Literal[1]
```
If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
```py
class D:
def copy(self, other: "D"):
self.x = other.x
reveal_type(D().x) # revealed: Unknown
```
If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to
that name, so these cases don't trigger any cycle:
```py
class E:
def __init__(self):
self.x: int = 1
def copy(self, other: "E"):
self.x = other.x
reveal_type(E().x) # revealed: int
class F:
def __init__(self):
self.x = 1
def copy(self, other: "F"):
self.x: int = other.x
reveal_type(F().x) # revealed: int
class G:
def copy(self, other: "G"):
self.x: int = other.x
reveal_type(G().x) # revealed: int
```
We can even handle cycles involving multiple classes:
```py
class A:
def __init__(self):
self.x = 1
def copy(self, other: "B"):
self.x = other.x
class B:
def copy(self, other: "A"):
self.x = other.x
reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]
```
This case additionally tests our union/intersection simplification logic:
```py
class H:
def __init__(self):
self.x = 1
def copy(self, other: "H"):
self.x = other.x or self.x
```
### Builtin types attributes
This test can probably be removed eventually, but we currently include it because we do not yet
@@ -1949,27 +1853,6 @@ reveal_type(C.a_type) # revealed: type
reveal_type(C.a_none) # revealed: None
```
### Generic methods
We also detect implicit instance attributes on methods that are themselves generic. We have an extra
test for this because generic functions have an extra type-params scope in between the function body
scope and the outer scope, so we need to make sure that our implementation can still recognize `f`
as a method of `C` here:
```toml
[environment]
python-version = "3.12"
```
```py
class C:
def f[T](self, t: T) -> T:
self.x: int = 1
return t
reveal_type(C().x) # revealed: int
```
## Enum classes
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
@@ -1987,6 +1870,20 @@ 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

View File

@@ -310,7 +310,9 @@ reveal_type(A() + 1) # revealed: A
reveal_type(1 + A()) # revealed: A
reveal_type(A() + "foo") # revealed: A
reveal_type("foo" + A()) # 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(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
@@ -318,14 +320,16 @@ 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(full tuple[...] support)
reveal_type(() + A()) # revealed: @Todo(return type of overloaded 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`:
reveal_type(literal_string_instance) # revealed: LiteralString
reveal_type(A() + literal_string_instance) # revealed: A
reveal_type(literal_string_instance + A()) # 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)
```
## Operations involving instances of classes inheriting from `Any`

View File

@@ -50,11 +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: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: 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

View File

@@ -43,7 +43,7 @@ if True and (x := 1):
```py
def _(flag: bool):
flag or (x := 1) or reveal_type(x) # revealed: Never
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
# error: [unresolved-reference]
flag or reveal_type(y) or (y := 1) # revealed: Unknown

View File

@@ -292,66 +292,3 @@ reveal_type(a) # revealed: Unknown
# Modifications allowed in this case:
a = None
```
## In stub files
In stub files, we have a minor modification to the rules above: we do not union with `Unknown` for
undeclared symbols.
### Undeclared and bound
`mod.pyi`:
```pyi
MyInt = int
class C:
MyStr = str
```
```py
from mod import MyInt, C
reveal_type(MyInt) # revealed: Literal[int]
reveal_type(C.MyStr) # revealed: Literal[str]
```
### Undeclared and possibly unbound
`mod.pyi`:
```pyi
def flag() -> bool:
return True
if flag():
MyInt = int
class C:
MyStr = str
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
from mod import MyInt, C
reveal_type(MyInt) # revealed: Literal[int]
reveal_type(C.MyStr) # revealed: Literal[str]
```
### Undeclared and unbound
`mod.pyi`:
```pyi
if False:
MyInt = int
```
```py
# error: [unresolved-import]
from mod import MyInt
reveal_type(MyInt) # revealed: Unknown
```

View File

@@ -1,25 +1,25 @@
# Constructor
When classes are instantiated, Python calls the metaclass's `__call__` method. The metaclass of most
Python classes is the class `builtins.type`.
When classes are instantiated, Python calls the meta-class `__call__` method, which can either be
customized by the user or `type.__call__` is used.
`type.__call__` calls the `__new__` method of the class, which is responsible for creating the
instance. `__init__` is then called on the constructed instance with the same arguments that were
passed to `__new__`.
The latter calls the `__new__` method of the class, which is responsible for creating the instance
and then calls the `__init__` method on the resulting instance to initialize it with the same
arguments.
Both `__new__` and `__init__` are looked up using the descriptor protocol, i.e., `__get__` is called
if these attributes are descriptors. `__new__` is always treated as a static method, i.e., `cls` is
passed as the first argument. `__init__` has no special handling; it is fetched as a bound method
and called just like any other dunder method.
Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then
called as an implicit static, rather than bound method with `cls` passed as the first argument.
`__init__` has no special handling, it is fetched as bound method and is called just like any other
dunder method.
`type.__call__` does other things too, but this is not yet handled by us.
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
`object.__init__`. They have some special behavior, namely:
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for
`object`), no arguments are accepted and `TypeError` is raised if any are passed.
- If `__new__` is defined but `__init__` is not, `object.__init__` will allow arbitrary arguments!
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`)
\- no arguments are accepted and `TypeError` is raised if any are passed.
- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments!
As of today there are a number of behaviors that we do not support:
@@ -146,25 +146,6 @@ reveal_type(Foo()) # revealed: Foo
### Possibly Unbound
#### Possibly unbound `__new__` method
```py
def _(flag: bool) -> None:
class Foo:
if flag:
def __new__(cls):
return object.__new__(cls)
# error: [call-possibly-unbound-method]
reveal_type(Foo()) # revealed: Foo
# error: [call-possibly-unbound-method]
# error: [too-many-positional-arguments]
reveal_type(Foo(1)) # revealed: Foo
```
#### Possibly unbound `__call__` on `__new__` callable
```py
def _(flag: bool) -> None:
class Callable:
@@ -342,86 +323,3 @@ reveal_type(Foo(1)) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
### Incompatible signatures
```py
import abc
class Foo:
def __new__(cls) -> "Foo":
return object.__new__(cls)
def __init__(self, x):
self.x = 42
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1"
reveal_type(Foo(42)) # revealed: Foo
class Foo2:
def __new__(cls, x) -> "Foo2":
return object.__new__(cls)
def __init__(self):
pass
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo2()) # revealed: Foo2
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
reveal_type(Foo2(42)) # revealed: Foo2
class Foo3(metaclass=abc.ABCMeta):
def __new__(cls) -> "Foo3":
return object.__new__(cls)
def __init__(self, x):
self.x = 42
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo3()) # revealed: Foo3
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1"
reveal_type(Foo3(42)) # revealed: Foo3
class Foo4(metaclass=abc.ABCMeta):
def __new__(cls, x) -> "Foo4":
return object.__new__(cls)
def __init__(self):
pass
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo4()) # revealed: Foo4
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
reveal_type(Foo4(42)) # revealed: Foo4
```
### Lookup of `__new__`
The `__new__` method is always invoked on the class itself, never on the metaclass. This is
different from how other dunder methods like `__lt__` are implicitly called (always on the
meta-type, never on the type itself).
```py
from typing_extensions import Literal
class Meta(type):
def __new__(mcls, name, bases, namespace, /, **kwargs):
return super().__new__(mcls, name, bases, namespace)
def __lt__(cls, other) -> Literal[True]:
return True
class C(metaclass=Meta): ...
# No error is raised here, since we don't implicitly call `Meta.__new__`
reveal_type(C()) # revealed: C
# Meta.__lt__ is implicitly called here:
reveal_type(C < C) # revealed: Literal[True]
```

View File

@@ -21,11 +21,6 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
## Generic
```toml
[environment]
python-version = "3.12"
```
```py
def get_int[T]() -> int:
return 42

View File

@@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd
methods, even though it is not available on `types.MethodType`:
```py
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
```
## Basic method calls on class objects and instances
@@ -205,7 +205,7 @@ reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
```pyi
```py
from types import FunctionType, MethodType
from typing import overload
@@ -399,11 +399,6 @@ reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound metho
### Classmethods mixed with other decorators
```toml
[environment]
python-version = "3.12"
```
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
class method:
@@ -415,19 +410,29 @@ 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"
reveal_type(C.f1(1)) # revealed: str
reveal_type(C().f1(1)) # revealed: str
reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
# TODO: All of these should be `str` (and not emit an error), once we support generics
# error: [call-non-callable]
reveal_type(C.f1(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C().f1(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C.f2(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C().f2(1)) # revealed: Unknown
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View File

@@ -162,44 +162,6 @@ def _(flag: bool):
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
```
## Unions with literals and negations
```py
from typing import Literal
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy]))
static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""]))
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]]))
static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]]))
static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""]))
static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""]))
def _(
a: Literal["a", ""] | Not[AlwaysFalsy],
b: Literal["a", ""] | Not[Literal[""]],
c: Literal[""] | Not[Literal[""]],
d: Not[Literal[""]] | Literal[""],
e: Literal["a"] | Not[Literal["a"]],
f: Literal[b"b"] | Not[Literal[b"b"]],
g: Not[Literal[b"b"]] | Literal[b"b"],
h: Literal[42] | Not[Literal[42]],
i: Not[Literal[42]] | Literal[42],
):
reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy
reveal_type(b) # revealed: object
reveal_type(c) # revealed: object
reveal_type(d) # revealed: object
reveal_type(e) # revealed: object
reveal_type(f) # revealed: object
reveal_type(g) # revealed: object
reveal_type(h) # revealed: object
reveal_type(i) # revealed: object
```
## Cannot use an argument as both a value and a type form
```py
@@ -213,41 +175,3 @@ def _(flag: bool):
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
reveal_type(f(int)) # revealed: str | Literal[True]
```
## Size limit on unions of literals
Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`,
`bytes`, `str`).
```py
from typing import Literal
def _(literals_2: Literal[0, 1], b: bool, flag: bool):
literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3]
literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15]
literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63]
literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127]
# Going beyond the MAX_UNION_LITERALS limit (currently 200):
literals_256 = 16 * literals_16 + literals_16
reveal_type(literals_256) # revealed: int
# Going beyond the limit when another type is already part of the union
bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127]
literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255]
# Now union the two:
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
```
## Simplifying gradually-equivalent types
If two types are gradually equivalent, we can keep just one of them in a union:
```py
from typing import Any, Union
from knot_extensions import Intersection, Not
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
reveal_type(x) # revealed: Any & ~int
```

View File

@@ -1,410 +0,0 @@
# Super
Python defines the terms *bound super object* and *unbound super object*.
An **unbound super object** is created when `super` is called with only one argument. (e.g.
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
rarely used in practice.
A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the
implicit form `super()`, where both the pivot class and the owner are inferred. This is the most
common usage.
## Basic Usage
### Explicit Super Object
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
specified pivot class.
```py
class A:
def a(self): ...
aa: int = 1
class B(A):
def b(self): ...
bb: int = 2
class C(B):
def c(self): ...
cc: int = 3
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
super(C, C()).a
super(C, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
super(C, C()).c
super(B, C()).a
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
super(B, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
super(B, C()).c
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
super(A, C()).a
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
super(A, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
super(A, C()).c
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
reveal_type(super(C, C()).aa) # revealed: int
reveal_type(super(C, C()).bb) # revealed: int
```
### Implicit Super Object
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
to the class that contains the function where `super()` is used. The first argument refers to the
current methods first parameter (typically `self` or `cls`).
```py
from __future__ import annotations
class A:
def __init__(self, a: int): ...
@classmethod
def f(cls): ...
class B(A):
def __init__(self, a: int):
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
super().__init__(a)
@classmethod
def f(cls):
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
super().f()
super(B, B(42)).__init__(42)
super(B, B).f()
```
### Unbound Super Object
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
a plain `super` instance and does not support name lookup via the MRO.
```py
class A:
a: int = 42
class B(A): ...
reveal_type(super(B)) # revealed: super
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
super(B).a
```
## Attribute Assignment
`super()` objects do not allow attribute assignment — even if the attribute is resolved
successfully.
```py
class A:
a: int = 3
class B(A): ...
reveal_type(super(B, B()).a) # revealed: int
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
super(B, B()).a = 3
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
super(B).a = 5
```
## Dynamic Types
If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a
member, it should effectively behave like a dynamic type.
```py
class A:
a: int = 1
def f(x):
reveal_type(x) # revealed: Unknown
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
reveal_type(super(x, x).a) # revealed: Unknown
reveal_type(super(A, x).a) # revealed: Unknown
reveal_type(super(x, A()).a) # revealed: Unknown
```
## Implicit `super()` in Complex Structure
```py
from __future__ import annotations
class A:
def test(self):
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
class B:
def test(self):
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
class C(A.B):
def test(self):
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
def inner(t: C):
reveal_type(super()) # revealed: <super: Literal[B], C>
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
```
## Built-ins and Literals
```py
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
```
## Descriptor Behavior with Super
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
differ depending on whether the second argument to `super` is a class or an instance.
```py
class A:
def a1(self): ...
@classmethod
def a2(cls): ...
class B(A): ...
# A.__dict__["a1"].__get__(B(), B)
reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown
# A.__dict__["a2"].__get__(B(), B)
reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
# A.__dict__["a1"].__get__(None, B)
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
# A.__dict__["a2"].__get__(None, B)
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
```
## Union of Supers
When the owner is a union type, `super()` is built separately for each branch, and the resulting
super objects are combined into a union.
```py
class A: ...
class B:
b: int = 42
class C(A, B): ...
class D(B, A): ...
def f(x: C | D):
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
s = super(A, x)
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
s.b
def f(flag: bool):
x = str() if flag else str("hello")
reveal_type(x) # revealed: Literal["", "hello"]
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
def f(x: int | str):
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
super(int, x)
```
Even when `super()` is constructed separately for each branch of a union, it should behave correctly
in all cases.
```py
def f(flag: bool):
if flag:
class A:
x = 1
y: int = 1
a: str = "hello"
class B(A): ...
s = super(B, B())
else:
class C:
x = 2
y: int | str = "test"
class D(C): ...
s = super(D, D())
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
reveal_type(s.y) # revealed: int | str
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
reveal_type(s.a) # revealed: str
```
## Supers with Generic Classes
```toml
[environment]
python-version = "3.12"
```
```py
from knot_extensions import TypeOf, static_assert, is_subtype_of
class A[T]:
def f(self, a: T) -> T:
return a
class B[T](A[T]):
def f(self, b: T) -> T:
return super().f(b)
```
## Invalid Usages
### Unresolvable `super()` Calls
If an appropriate class and argument cannot be found, a runtime error will occur.
```py
from __future__ import annotations
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
reveal_type(super()) # revealed: Unknown
def f():
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
super()
# No first argument in its scope
class A:
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
s = super()
def f(self):
def g():
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
super()
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
lambda: super()
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
(super() for _ in range(10))
@staticmethod
def h():
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
super()
```
### Failing Condition Checks
```toml
[environment]
python-version = "3.12"
```
`super()` requires its first argument to be a valid class, and its second argument to be either an
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
runtime.
```py
def f(x: int):
# error: [invalid-super-argument] "`int` is not a valid class"
super(x, x)
type IntAlias = int
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
super(IntAlias, 0)
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
# revealed: Unknown
reveal_type(super(int, str()))
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
# revealed: Unknown
reveal_type(super(int, str))
class A: ...
class B(A): ...
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
# revealed: Unknown
reveal_type(super(B, A()))
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
# revealed: Unknown
reveal_type(super(B, object()))
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
# revealed: Unknown
reveal_type(super(B, A))
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
# revealed: Unknown
reveal_type(super(B, object))
super(object, object()).__class__
```
### Instance Member Access via `super`
Accessing instance members through `super()` is not allowed.
```py
from __future__ import annotations
class A:
def __init__(self, a: int):
self.a = a
class B(A):
def __init__(self, a: int):
super().__init__(a)
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
super().a
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
super(B, B(42)).a
```
### Dunder Method Resolution
Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super
object itself. In other words, `super` should not be treated as if it inherits attributes of the
`owner`.
```py
class A:
def __getitem__(self, key: int) -> int:
return 42
class B(A): ...
reveal_type(A()[0]) # revealed: int
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
super(B, B())[0]
```

View File

@@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
```
## Integer instance

View File

@@ -50,17 +50,13 @@ reveal_type(x) # revealed: LiteralString
if x != "abc":
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
# TODO: This should be `Literal[False]`
reveal_type(x == "abc") # revealed: bool
# TODO: This should be `Literal[False]`
reveal_type("abc" == x) # revealed: bool
reveal_type(x == "abc") # revealed: Literal[False]
reveal_type("abc" == x) # revealed: Literal[False]
reveal_type(x == "something else") # revealed: bool
reveal_type("something else" == x) # revealed: bool
# TODO: This should be `Literal[True]`
reveal_type(x != "abc") # revealed: bool
# TODO: This should be `Literal[True]`
reveal_type("abc" != x) # revealed: bool
reveal_type(x != "abc") # revealed: Literal[True]
reveal_type("abc" != x) # revealed: Literal[True]
reveal_type(x != "something else") # revealed: bool
reveal_type("something else" != x) # revealed: bool
@@ -83,10 +79,10 @@ def _(x: int):
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
reveal_type(x != 1) # revealed: bool
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 2) # revealed: bool
reveal_type(x == 1) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 2) # revealed: bool
```

View File

@@ -37,7 +37,7 @@ class C:
return self
x = A() < B() < C()
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: Literal[False] | A

View File

@@ -127,9 +127,8 @@ class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
```
### Invalid async comprehension
@@ -146,7 +145,6 @@ class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
```

View File

@@ -42,6 +42,6 @@ def _(flag: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
3 if NotBoolable() else 4
```

View File

@@ -154,10 +154,10 @@ def _(flag: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
...
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
elif NotBoolable():
...
```

View File

@@ -1,10 +1,5 @@
# Pattern matching
```toml
[environment]
python-version = "3.10"
```
## With wildcard
```py
@@ -292,7 +287,7 @@ class NotBoolable:
def _(target: int, flag: NotBoolable):
y = 1
match target:
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
case 1 if flag:
y = 2
case 2:

View File

@@ -1,293 +0,0 @@
# `typing.dataclass_transform`
```toml
[environment]
python-version = "3.12"
```
`dataclass_transform` is a decorator that can be used to let type checkers know that a function,
class, or metaclass is a `dataclass`-like construct.
## Basic example
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
# modify cls
return cls
@my_dataclass
class Person:
name: str
age: int | None = None
Person("Alice", 20)
Person("Bob", None)
Person("Bob")
# error: [missing-argument]
Person()
```
## Decorating decorators that take parameters themselves
If we want our `dataclass`-like decorator to also take parameters, that is also possible:
```py
from typing_extensions import dataclass_transform, Callable
@dataclass_transform()
def versioned_class[T](*, version: int = 1):
def decorator(cls):
# modify cls
return cls
return decorator
@versioned_class(version=2)
class Person:
name: str
age: int | None = None
Person("Alice", 20)
# error: [missing-argument]
Person()
```
We properly type-check the arguments to the decorator:
```py
from typing_extensions import dataclass_transform, Callable
# error: [invalid-argument-type]
@versioned_class(version="a string")
class C:
name: str
```
## Types of decorators
The examples from this section are straight from the Python documentation on
[`typing.dataclass_transform`].
### Decorating a decorator function
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def create_model[T](cls: type[T]) -> type[T]:
...
return cls
@create_model
class CustomerModel:
id: int
name: str
CustomerModel(id=1, name="Test")
```
### Decorating a metaclass
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelMeta(type): ...
class ModelBase(metaclass=ModelMeta): ...
class CustomerModel(ModelBase):
id: int
name: str
CustomerModel(id=1, name="Test")
# error: [missing-argument]
CustomerModel()
```
### Decorating a base class
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelBase: ...
class CustomerModel(ModelBase):
id: int
name: str
# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")
```
## Arguments to `dataclass_transform`
### `eq_default`
`eq=True/False` does not have a observable effect (apart from a minor change regarding whether
`other` is positional-only or not, which is not modelled at the moment).
### `order_default`
The `order_default` argument controls whether methods such as `__lt__` are generated by default.
This can be overwritten using the `order` argument to the custom decorator:
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def normal(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=False)
def order_default_false(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=True)
def order_default_true(*, order: bool = True):
raise NotImplementedError
@normal
class Normal:
inner: int
Normal(1) < Normal(2) # error: [unsupported-operator]
@normal(order=True)
class NormalOverwritten:
inner: int
NormalOverwritten(1) < NormalOverwritten(2)
@order_default_false
class OrderFalse:
inner: int
OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator]
@order_default_false(order=True)
class OrderFalseOverwritten:
inner: int
OrderFalseOverwritten(1) < OrderFalseOverwritten(2)
@order_default_true
class OrderTrue:
inner: int
OrderTrue(1) < OrderTrue(2)
@order_default_true(order=False)
class OrderTrueOverwritten:
inner: int
# error: [unsupported-operator]
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)
```
### `kw_only_default`
To do
### `field_specifiers`
To do
## Overloaded dataclass-like decorators
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
implementation, or to *one* of the overloads.
### Applying `dataclass_transform` to the implementation
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
@dataclass_transform()
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
D1("a")
D2("a")
D1(1.2) # error: [invalid-argument-type]
D2(1.2) # error: [invalid-argument-type]
```
### Applying `dataclass_transform` to an overload
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
@dataclass_transform()
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
# TODO: these should not be errors
D1("a") # error: [too-many-positional-arguments]
D2("a") # error: [too-many-positional-arguments]
# TODO: these should be invalid-argument-type errors
D1(1.2) # error: [too-many-positional-arguments]
D2(1.2) # error: [too-many-positional-arguments]
```
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform

View File

@@ -1,731 +0,0 @@
# Dataclasses
## Basic
Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`,
`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass`
decorator. By default, only the three mentioned methods are generated.
```py
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int | None = None
alice1 = Person("Alice", 30)
alice2 = Person(name="Alice", age=30)
alice3 = Person(age=30, name="Alice")
alice4 = Person("Alice", age=30)
reveal_type(alice1) # revealed: Person
reveal_type(type(alice1)) # revealed: type[Person]
reveal_type(alice1.name) # revealed: str
reveal_type(alice1.age) # revealed: int | None
reveal_type(repr(alice1)) # revealed: str
reveal_type(alice1 == alice2) # revealed: bool
reveal_type(alice1 == "Alice") # revealed: bool
bob = Person("Bob")
bob2 = Person("Bob", None)
bob3 = Person(name="Bob")
bob4 = Person(name="Bob", age=None)
```
The signature of the `__init__` method is generated based on the classes attributes. The following
calls are not valid:
```py
# error: [missing-argument]
Person()
# error: [too-many-positional-arguments]
Person("Eve", 20, "too many arguments")
# error: [invalid-argument-type]
Person("Eve", "string instead of int")
# error: [invalid-argument-type]
# error: [invalid-argument-type]
Person(20, "Eve")
```
## Signature of `__init__`
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
Declarations in the class body are used to generate the signature of the `__init__` method. If the
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
the default value.
```py
from dataclasses import dataclass
@dataclass
class D:
x: int
y: str = "default"
z: int | None = 1 + 2
reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
```
This also works if the declaration and binding are split:
```py
@dataclass
class D:
x: int | None
x = None
reveal_type(D.__init__) # revealed: (x: int | None = None) -> None
```
Non-fully static types are handled correctly:
```py
from typing import Any
@dataclass
class C:
x: Any
y: int | Any
z: tuple[int, Any]
reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None
```
Variables without annotations are ignored:
```py
@dataclass
class D:
x: int
y = 1
reveal_type(D.__init__) # revealed: (x: int) -> None
```
If attributes without default values are declared after attributes with default values, a
`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case:
```py
@dataclass
class D:
x: int = 1
# TODO: this should be an error: field without default defined after field with default
y: str
```
Pure class attributes (`ClassVar`) are not included in the signature of `__init__`:
```py
from typing import ClassVar
@dataclass
class D:
x: int
y: ClassVar[str] = "default"
z: bool
reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None
d = D(1, True)
reveal_type(d.x) # revealed: int
reveal_type(d.y) # revealed: str
reveal_type(d.z) # revealed: bool
```
Function declarations do not affect the signature of `__init__`:
```py
@dataclass
class D:
x: int
def y(self) -> str:
return ""
reveal_type(D.__init__) # revealed: (x: int) -> None
```
And neither do nested class declarations:
```py
@dataclass
class D:
x: int
class Nested:
y: str
reveal_type(D.__init__) # revealed: (x: int) -> None
```
But if there is a variable annotation with a function or class literal type, the signature of
`__init__` will include this field:
```py
from knot_extensions import TypeOf
class SomeClass: ...
def some_function() -> None: ...
@dataclass
class D:
function_literal: TypeOf[some_function]
class_literal: TypeOf[SomeClass]
class_subtype_of: type[SomeClass]
# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None
reveal_type(D.__init__)
```
More realistically, dataclasses can have `Callable` attributes:
```py
from typing import Callable
@dataclass
class D:
c: Callable[[int], str]
reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None
```
Implicit instance attributes do not affect the signature of `__init__`:
```py
@dataclass
class D:
x: int
def f(self, y: str) -> None:
self.y: str = y
reveal_type(D(1).y) # revealed: str
reveal_type(D.__init__) # revealed: (x: int) -> None
```
Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't
be included in the signature of `__init__`. This is a case that we currently don't detect:
```py
@dataclass
class D:
# (x) is an expression, not a "simple name"
(x): int = 1
# TODO: should ideally not include a `x` parameter
reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None
```
## `@dataclass` calls with arguments
The `@dataclass` decorator can take several arguments to customize the existence of the generated
methods. The following test makes sure that we still treat the class as a dataclass if (the default)
arguments are passed in:
```py
from dataclasses import dataclass
@dataclass(init=True, repr=True, eq=True)
class Person:
name: str
age: int | None = None
alice = Person("Alice", 30)
reveal_type(repr(alice)) # revealed: str
reveal_type(alice == alice) # revealed: bool
```
If `init` is set to `False`, no `__init__` method is generated:
```py
from dataclasses import dataclass
@dataclass(init=False)
class C:
x: int
C() # Okay
# error: [too-many-positional-arguments]
C(1)
repr(C())
C() == C()
```
## Other dataclass parameters
### `repr`
A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but
in that case `__repr__` is still available via `object.__repr__`:
```py
from dataclasses import dataclass
@dataclass(repr=False)
class WithoutRepr:
x: int
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
```
### `eq`
The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but
`__eq__` is still available via `object.__eq__`:
```py
from dataclasses import dataclass
@dataclass(eq=False)
class WithoutEq:
x: int
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
```
### `order`
```toml
[environment]
python-version = "3.12"
```
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
methods will be generated:
```py
from dataclasses import dataclass
@dataclass
class WithoutOrder:
x: int
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
@dataclass(order=True)
class WithOrder:
x: int
WithOrder(1) < WithOrder(2)
WithOrder(1) <= WithOrder(2)
WithOrder(1) > WithOrder(2)
WithOrder(1) >= WithOrder(2)
```
Comparisons are only allowed for `WithOrder` instances:
```py
WithOrder(1) < 2 # error: [unsupported-operator]
WithOrder(1) <= 2 # error: [unsupported-operator]
WithOrder(1) > 2 # error: [unsupported-operator]
WithOrder(1) >= 2 # error: [unsupported-operator]
```
This also works for generic dataclasses:
```py
from dataclasses import dataclass
@dataclass(order=True)
class GenericWithOrder[T]:
x: T
GenericWithOrder[int](1) < GenericWithOrder[int](1)
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
```
If a class already defines one of the comparison methods, a `TypeError` is raised at runtime.
Ideally, we would emit a diagnostic in that case:
```py
@dataclass(order=True)
class AlreadyHasCustomDunderLt:
x: int
# TODO: Ideally, we would emit a diagnostic here
def __lt__(self, other: object) -> bool:
return False
```
### `unsafe_hash`
To do
### `frozen`
To do
### `match_args`
To do
### `kw_only`
To do
### `slots`
To do
### `weakref_slot`
To do
## Inheritance
### Normal class inheriting from a dataclass
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
class Derived(Base): ...
d = Derived(1) # OK
reveal_type(d.x) # revealed: int
```
### Dataclass inheriting from normal class
```py
from dataclasses import dataclass
class Base:
x: int = 1
@dataclass
class Derived(Base):
y: str
d = Derived("a")
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
Derived(1, "a")
```
### Dataclass inheriting from another dataclass
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
y: str
@dataclass
class Derived(Base):
z: bool
d = Derived(1, "a", True) # OK
reveal_type(d.x) # revealed: int
reveal_type(d.y) # revealed: str
reveal_type(d.z) # revealed: bool
# error: [missing-argument]
Derived(1, "a")
# error: [missing-argument]
Derived(True)
```
### Overwriting attributes from base class
The following example comes from the
[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x`
attribute appears just once in the `__init__` signature, and the default value is taken from the
derived class
```py
from dataclasses import dataclass
from typing import Any
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
```
## Generic dataclasses
```toml
[environment]
python-version = "3.12"
```
```py
from dataclasses import dataclass
@dataclass
class DataWithDescription[T]:
data: T
description: str
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
d_int = DataWithDescription[int](1, "description") # OK
reveal_type(d_int.data) # revealed: int
reveal_type(d_int.description) # revealed: str
# error: [invalid-argument-type]
DataWithDescription[int](None, "description")
```
## Descriptor-typed fields
### Same type in `__get__` and `__set__`
For the following descriptor, the return type of `__get__` and the type of the `value` parameter in
`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of
the type of the descriptor), and the default value is also of this type:
```py
from typing import overload
from dataclasses import dataclass
class UppercaseString:
_value: str = ""
def __get__(self, instance: object, owner: None | type) -> str:
return self._value
def __set__(self, instance: object, value: str) -> None:
self._value = value.upper()
@dataclass
class C:
upper: UppercaseString = UppercaseString()
reveal_type(C.__init__) # revealed: (upper: str = str) -> None
c = C("abc")
reveal_type(c.upper) # revealed: str
# This is also okay:
C()
# error: [invalid-argument-type]
C(1)
# error: [too-many-positional-arguments]
C("a", "b")
```
### Different types in `__get__` and `__set__`
In general, the type of the `__init__` parameter is determined by the `value` parameter type of the
`__set__` method (`str` in the example below). However, the default value is generated by calling
the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None`
for the `instance` argument.
```py
from typing import Literal, overload
from dataclasses import dataclass
class ConvertToLength:
_len: int = 0
@overload
def __get__(self, instance: None, owner: type) -> Literal[""]: ...
@overload
def __get__(self, instance: object, owner: type | None) -> int: ...
def __get__(self, instance: object | None, owner: type | None) -> str | int:
if instance is None:
return ""
return self._len
def __set__(self, instance, value: str) -> None:
self._len = len(value)
@dataclass
class C:
converter: ConvertToLength = ConvertToLength()
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
c = C("abc")
reveal_type(c.converter) # revealed: int
# This is also okay:
C()
# error: [invalid-argument-type]
C(1)
# error: [too-many-positional-arguments]
C("a", "b")
```
### With overloaded `__set__` method
If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the
union of all possible `value` parameter types:
```py
from typing import overload
from dataclasses import dataclass
class AcceptsStrAndInt:
def __get__(self, instance, owner) -> int:
return 0
@overload
def __set__(self, instance: object, value: str) -> None: ...
@overload
def __set__(self, instance: object, value: int) -> None: ...
def __set__(self, instance: object, value) -> None:
pass
@dataclass
class C:
field: AcceptsStrAndInt = AcceptsStrAndInt()
reveal_type(C.__init__) # revealed: (field: str | int = int) -> None
```
## `dataclasses.field`
To do
## Other special cases
### `dataclasses.dataclass`
We also understand dataclasses if they are decorated with the fully qualified name:
```py
import dataclasses
@dataclasses.dataclass
class C:
x: str
reveal_type(C.__init__) # revealed: (x: str) -> None
```
### Dataclass with custom `__init__` method
If a class already defines `__init__`, it is not replaced by the `dataclass` decorator.
```py
from dataclasses import dataclass
@dataclass(init=True)
class C:
x: str
def __init__(self, x: int) -> None:
self.x = str(x)
C(1) # OK
# error: [invalid-argument-type]
C("a")
```
Similarly, if we set `init=False`, we still recognize the custom `__init__` method:
```py
@dataclass(init=False)
class D:
def __init__(self, x: int) -> None:
self.x = str(x)
D(1) # OK
D() # error: [missing-argument]
```
### Accessing instance attributes on the class itself
Just like for normal classes, accessing instance attributes on the class itself is not allowed:
```py
from dataclasses import dataclass
@dataclass
class C:
x: int
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself."
C.x
```
### Return type of `dataclass(...)`
A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator.
We can store the callable in a variable and later use it as a decorator:
```py
from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
@dataclass_with_order
class C:
x: int
C(1) < C(2) # ok
```
### Using `dataclass` as a function
To do
## Internals
The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`,
and attributes like the MRO are unchanged:
```py
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int | None = None
reveal_type(type(Person)) # revealed: Literal[type]
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
```
The generated methods have the following signatures:
```py
# TODO: `self` is missing here
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
```

View File

@@ -145,10 +145,10 @@ def f(x: int) -> int:
return x**2
# TODO: Should be `_lru_cache_wrapper[int]`
reveal_type(f) # revealed: @Todo(specialized non-generic class)
reveal_type(f) # revealed: @Todo(generics)
# TODO: Should be `int`
reveal_type(f(1)) # revealed: @Todo(specialized non-generic class)
reveal_type(f(1)) # revealed: @Todo(generics)
```
## Lambdas as decorators

View File

@@ -459,9 +459,11 @@ class Descriptor:
class C:
d: Descriptor = Descriptor()
reveal_type(C.d) # revealed: Literal["called on class object"]
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: LiteralString
reveal_type(C().d) # revealed: Literal["called on instance"]
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: LiteralString
```
## Descriptor protocol for dunder methods

View File

@@ -1,221 +0,0 @@
# Semantic syntax error diagnostics
## `async` comprehensions in synchronous comprehensions
### Python 3.10
<!-- snapshot-diagnostics -->
Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even
within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)):
```toml
[environment]
python-version = "3.10"
```
```py
async def elements(n):
yield n
async def f():
# error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)"
return {n: [x async for x in elements(n)] for n in range(3)}
```
If all of the comprehensions are `async`, on the other hand, the code was still valid:
```py
async def test():
return [[x async for x in elements(n)] async for n in range(3)]
```
These are a couple of tricky but valid cases to check that nested scope handling is wired up
correctly in the `SemanticSyntaxContext` trait:
```py
async def f():
[x for x in [1]] and [x async for x in elements(1)]
async def f():
def g():
pass
[x async for x in elements(1)]
```
### Python 3.11
All of these same examples are valid after Python 3.11:
```toml
[environment]
python-version = "3.11"
```
```py
async def elements(n):
yield n
async def f():
return {n: [x async for x in elements(n)] for n in range(3)}
```
## Late `__future__` import
```py
from collections import namedtuple
# error: [invalid-syntax] "__future__ imports must be at the top of the file"
from __future__ import print_function
```
## Invalid annotation
This one might be a bit redundant with the `invalid-type-form` error.
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a type annotation"
def f() -> (y := 3): ...
```
## Duplicate `match` key
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
# error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`"
case {"x": 1, "x": 2}:
...
```
## `return`, `yield`, `yield from`, and `await` outside function
```py
# error: [invalid-syntax] "`return` statement outside of a function"
return
# error: [invalid-syntax] "`yield` statement outside of a function"
yield
# error: [invalid-syntax] "`yield from` statement outside of a function"
yield from []
# error: [invalid-syntax] "`await` statement outside of a function"
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
def f():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
```
Generators are evaluated lazily, so `await` is allowed, even outside of a function.
```py
async def g():
yield 1
(x async for x in g())
```
## Rebound comprehension variable
Walrus operators cannot rebind variables already in use as iterators:
```py
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
[x := 2 for x in range(10)]
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
{y := 5 for y in range(10)}
```
## Multiple case assignments
Variable names in pattern matching must be unique within a single pattern:
```toml
[environment]
python-version = "3.10"
```
```py
x = [1, 2]
match x:
# error: [invalid-syntax] "multiple assignments to name `a` in pattern"
case [a, a]:
pass
case _:
pass
d = {"key": "value"}
match d:
# error: [invalid-syntax] "multiple assignments to name `b` in pattern"
case {"key": b, "other": b}:
pass
```
## Duplicate type parameter
Type parameter names must be unique in a generic class or function definition:
```toml
[environment]
python-version = "3.12"
```
```py
# error: [invalid-syntax] "duplicate type parameter"
class C[T, T]:
pass
# error: [invalid-syntax] "duplicate type parameter"
def f[X, Y, X]():
pass
```
## `await` outside async function
This error includes `await`, `async for`, `async with`, and `async` comprehensions.
```python
async def elements(n):
yield n
def _():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
# error: [invalid-syntax] "`async for` outside of an asynchronous function"
async for _ in elements(1):
...
# error: [invalid-syntax] "`async with` outside of an asynchronous function"
async with elements(1) as x:
...
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
# error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function"
[x async for x in elements(1)]
```
## Load before `global` declaration
This should be an error, but it's not yet.
TODO implement `SemanticSyntaxContext::global`
```py
def f():
x = 1
global x
```

View File

@@ -1,19 +0,0 @@
# Shadowing
<!-- snapshot-diagnostics -->
## Implicit class shadowing
```py
class C: ...
C = 1 # error: [invalid-assignment]
```
## Implicit function shadowing
```py
def f(): ...
f = 1 # error: [invalid-assignment]
```

View File

@@ -8,20 +8,14 @@
a, b = 1 # error: [not-iterable]
```
## Exactly too many values to unpack
## Too many values to unpack
```py
a, b = (1, 2, 3) # error: [invalid-assignment]
```
## Exactly too few values to unpack
## Too few values to unpack
```py
a, b = (1,) # error: [invalid-assignment]
```
## Too few values to unpack
```py
[a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```

View File

@@ -1,61 +0,0 @@
<!-- snapshot-diagnostics -->
# Different ways that `unsupported-bool-conversion` can occur
## Has a `__bool__` method, but has incorrect parameters
```py
class NotBoolable:
def __bool__(self, foo):
return False
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Has a `__bool__` method, but has an incorrect return type
```py
class NotBoolable:
def __bool__(self) -> str:
return "wat"
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Has a `__bool__` attribute, but it's not callable
```py
class NotBoolable:
__bool__: int = 3
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Part of a union where at least one member has incorrect `__bool__` method
```py
class NotBoolable1:
def __bool__(self) -> str:
return "wat"
class NotBoolable2:
pass
class NotBoolable3:
__bool__: int = 3
def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3:
return NotBoolable2()
# error: [unsupported-bool-conversion]
10 and get() and True
```

View File

@@ -1,37 +0,0 @@
# Version-related syntax error diagnostics
## `match` statement
The `match` statement was introduced in Python 3.10.
### Before 3.10
<!-- snapshot-diagnostics -->
We should emit a syntax error before 3.10.
```toml
[environment]
python-version = "3.9"
```
```py
match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
case 1:
print("it's one")
```
### After 3.10
On or after 3.10, no error should be reported.
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
case 1:
print("it's one")
```

View File

@@ -26,11 +26,6 @@ def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
## Use case: Type narrowing and exhaustiveness checking
```toml
[environment]
python-version = "3.10"
```
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.

View File

@@ -61,7 +61,7 @@ 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(specialized non-generic class)
reveal_type(a) # revealed: @Todo(generics)
b = cast(Any, y)
reveal_type(b) # revealed: Any

View File

@@ -4,6 +4,6 @@
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
assert NotBoolable()
```

View File

@@ -10,8 +10,8 @@ def _(foo: str):
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
```
## AND
@@ -20,8 +20,8 @@ def _(foo: str):
def _(foo: str):
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True]
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]
@@ -123,7 +123,7 @@ if NotBoolable():
class NotBoolable:
__bool__: None = None
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
...
```
@@ -135,7 +135,7 @@ def test(cond: bool):
class NotBoolable:
__bool__: int | None = None if cond else 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
...
```
@@ -149,7 +149,7 @@ def test(cond: bool):
a = 10 if cond else NotBoolable()
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
if a:
...
```

View File

@@ -76,11 +76,6 @@ def g(x: Any = "foo"):
## Stub functions
```toml
[environment]
python-version = "3.12"
```
### In Protocol
```py

View File

@@ -56,11 +56,6 @@ def f() -> int:
### In Protocol
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol, TypeVar
@@ -74,6 +69,8 @@ class Baz(Bar):
T = TypeVar("T")
class Qux(Protocol[T]):
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
class Foo(Protocol):
@@ -88,11 +85,6 @@ class Lorem(t[0]):
### In abstract method
```toml
[environment]
python-version = "3.12"
```
```py
from abc import ABC, abstractmethod
@@ -203,7 +195,7 @@ from typing import TypeVar
T = TypeVar("T")
# error: [invalid-return-type]
# TODO: `invalid-return-type` error should be emitted
def m(x: T) -> T: ...
```

View File

@@ -1,10 +1,5 @@
# Generic classes
```toml
[environment]
python-version = "3.13"
```
## PEP 695 syntax
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
@@ -45,6 +40,8 @@ from typing import Generic, TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-base]
class C(Generic[T]): ...
```
@@ -152,92 +149,23 @@ If a typevar does not provide a default, we use `Unknown`:
reveal_type(C()) # revealed: C[Unknown]
```
## Inferring generic class parameters from constructors
If the type of a constructor parameter is a class typevar, we can use that to infer the type
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
## `__new__` only
parameter:
```py
class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## `__init__` only
```py
class C[T]:
class E[T]:
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
# TODO: revealed: E[int] or E[Literal[1]]
reveal_type(E(1)) # revealed: E[Unknown]
```
## Identical `__new__` and `__init__` signatures
The types inferred from a type context and from a constructor parameter must be consistent with each
other:
```py
class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## Compatible `__new__` and `__init__` signatures
```py
class C[T]:
def __new__(cls, *args, **kwargs) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D[T]:
def __new__(cls, x: T) -> "D[T]":
return object.__new__(cls)
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
## `__init__` is itself generic
TODO: These do not currently work yet, because we don't correctly model the nested generic contexts.
```py
class C[T]:
def __init__[S](self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
# TODO: error: [invalid-argument-type]
wrong_innards: E[int] = E("five")
```
## Generic subclass
@@ -272,20 +200,21 @@ class C[T]:
def cannot_shadow_class_typevar[T](self, t: T): ...
c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
# TODO: no error
# TODO: revealed: str or Literal["string"]
# error: [invalid-argument-type]
reveal_type(c.method("string")) # revealed: U
```
## Cyclic class definitions
### F-bounded quantification
## Cyclic class definition
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
#### In a stub file
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
`stub.pyi`:
```pyi
class Base[T]: ...
class Sub(Base[Sub]): ...
@@ -293,10 +222,10 @@ class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### With string forward references
A similar case can work in a non-stub file, if forward references are stringified:
`string_annotation.py`:
```py
class Base[T]: ...
class Sub(Base["Sub"]): ...
@@ -304,10 +233,10 @@ class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### Without string forward references
In a non-stub file, without stringified forward references, this raises a `NameError`:
`bare_annotation.py`:
```py
class Base[T]: ...
@@ -315,23 +244,13 @@ class Base[T]: ...
class Sub(Base[Sub]): ...
```
### Cyclic inheritance as a generic parameter
## Another cyclic case
```pyi
# TODO no error (generics)
# error: [invalid-base]
class Derived[T](list[Derived[T]]): ...
```
### Direct cyclic inheritance
Inheritance that would result in a cyclic MRO is detected as an error.
```py
# error: [cyclic-class-definition]
class C[T](C): ...
# error: [cyclic-class-definition]
class D[T](D[int]): ...
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View File

@@ -1,10 +1,5 @@
# Generic functions
```toml
[environment]
python-version = "3.12"
```
## Typevar must be used at least twice
If you're only using a typevar for a single parameter, you don't need the typevar — just use
@@ -48,14 +43,33 @@ def absurd[T]() -> T:
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
is bound to at each call site.
TODO: Note that some of the TODO revealed types have two options, since we haven't decided yet
whether we want to infer a more specific `Literal` type where possible, or use heuristics to weaken
the inferred type to e.g. `int`.
```py
def f[T](x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(1.0)) # revealed: float
reveal_type(f(True)) # revealed: Literal[True]
reveal_type(f("string")) # revealed: Literal["string"]
# TODO: no error
# TODO: revealed: int or Literal[1]
# error: [invalid-argument-type]
reveal_type(f(1)) # revealed: T
# TODO: no error
# TODO: revealed: float
# error: [invalid-argument-type]
reveal_type(f(1.0)) # revealed: T
# TODO: no error
# TODO: revealed: bool or Literal[true]
# error: [invalid-argument-type]
reveal_type(f(True)) # revealed: T
# TODO: no error
# TODO: revealed: str or Literal["string"]
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: T
```
## Inferring “deep” generic parameter types
@@ -68,40 +82,7 @@ def f[T](x: list[T]) -> T:
return x[0]
# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: Unknown
```
## Inferring a bound typevar
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def f[T: int](x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(True)) # revealed: Literal[True]
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
reveal_type(f("string")) # revealed: Unknown
```
## Inferring a constrained typevar
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def f[T: (int, None)](x: T) -> T:
return x
reveal_type(f(1)) # revealed: int
reveal_type(f(True)) # revealed: int
reveal_type(f(None)) # revealed: None
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
reveal_type(f("string")) # revealed: Unknown
reveal_type(f([1.0, 2.0])) # revealed: T
```
## Typevar constraints
@@ -112,6 +93,7 @@ in the function.
```py
def good_param[T: int](x: T) -> None:
# TODO: revealed: T & int
reveal_type(x) # revealed: T
```
@@ -180,41 +162,61 @@ parameters simultaneously.
def two_params[T](x: T, y: T) -> T:
return x
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
```
# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(two_params("a", "b")) # revealed: T
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
all of the constraints.
```py
def union_param[T](x: T | None) -> T:
if x is None:
raise ValueError
return x
reveal_type(union_param("a")) # revealed: Literal["a"]
reveal_type(union_param(1)) # revealed: Literal[1]
reveal_type(union_param(None)) # revealed: Unknown
# TODO: no error
# TODO: revealed: str | int
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(two_params("a", 1)) # revealed: T
```
```py
def union_and_nonunion_params[T](x: T | int, y: T) -> T:
def param_with_union[T](x: T | int, y: T) -> T:
return y
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
reveal_type(param_with_union(1, "a")) # revealed: T
# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(param_with_union("a", "a")) # revealed: T
# TODO: no error
# TODO: revealed: int
# error: [invalid-argument-type]
reveal_type(param_with_union(1, 1)) # revealed: T
# TODO: no error
# TODO: revealed: str | int
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(param_with_union("a", 1)) # revealed: T
```
```py
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
return y
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
# TODO: no error
# TODO: revealed: tuple[str, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[T, S]
# TODO: no error
# TODO: revealed: tuple[str, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[T, S]
```
## Inferring nested generic function calls
@@ -229,6 +231,15 @@ def f[T](x: T) -> tuple[T, int]:
def g[T](x: T) -> T | None:
return x
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
# TODO: no error
# TODO: revealed: tuple[str | None, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(f(g("a"))) # revealed: tuple[T, int]
# TODO: no error
# TODO: revealed: tuple[str, int] | None
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(g(f("a"))) # revealed: T | None
```

View File

@@ -19,9 +19,6 @@ in newer Python releases.
from typing import TypeVar
T = TypeVar("T")
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Directly assigned to a variable
@@ -32,12 +29,7 @@ reveal_type(T.__name__) # revealed: Literal["T"]
```py
from typing import TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
# TODO: error
TestList = list[TypeVar("W")]
```
@@ -48,7 +40,7 @@ TestList = list[TypeVar("W")]
```py
from typing import TypeVar
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
# TODO: error
T = TypeVar("Q")
```
@@ -65,52 +57,6 @@ T = TypeVar("T")
T = TypeVar("T")
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.
```toml
[environment]
python-version = "3.13"
```
```py
from typing import TypeVar
T = TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__default__) # revealed: NoDefault
```
### Type variables with an upper bound
```py
from typing import TypeVar
T = TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__bound__) # revealed: None
```
### Type variables with constraints
```py
from typing import TypeVar
T = TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]
S = TypeVar("S")
reveal_type(S.__constraints__) # revealed: tuple[()]
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should

View File

@@ -1,10 +1,5 @@
# PEP 695 Generics
```toml
[environment]
python-version = "3.12"
```
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
## Type variables
@@ -17,51 +12,10 @@ instances of `typing.TypeVar`, just like legacy type variables.
```py
def f[T]():
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: T
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.
```toml
[environment]
python-version = "3.13"
```
```py
def f[T = int]():
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
def g[S]():
reveal_type(S.__default__) # revealed: NoDefault
```
### Type variables with an upper bound
```py
def f[T: int]():
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
def g[S]():
reveal_type(S.__bound__) # revealed: None
```
### Type variables with constraints
```py
def f[T: (int, str)]():
reveal_type(T.__constraints__) # revealed: tuple[int, str]
reveal_type(T.__bound__) # revealed: None
def g[S]():
reveal_type(S.__constraints__) # revealed: tuple[()]
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
@@ -105,19 +59,19 @@ is.)
from knot_extensions import is_fully_static, static_assert
from typing import Any
def unbounded_unconstrained[T](t: T) -> None:
def unbounded_unconstrained[T](t: list[T]) -> None:
static_assert(is_fully_static(T))
def bounded[T: int](t: T) -> None:
def bounded[T: int](t: list[T]) -> None:
static_assert(is_fully_static(T))
def bounded_by_gradual[T: Any](t: T) -> None:
def bounded_by_gradual[T: Any](t: list[T]) -> None:
static_assert(not is_fully_static(T))
def constrained[T: (int, str)](t: T) -> None:
def constrained[T: (int, str)](t: list[T]) -> None:
static_assert(is_fully_static(T))
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
static_assert(not is_fully_static(T))
```
@@ -140,7 +94,7 @@ class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
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))
@@ -170,7 +124,7 @@ is a final class, since the typevar can still be specialized to `Never`.)
from typing import Any
from typing_extensions import final
def bounded[T: Super](t: T) -> None:
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))
@@ -181,7 +135,7 @@ def bounded[T: Super](t: T) -> None:
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Sub, T))
def bounded_by_gradual[T: Any](t: T) -> None:
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))
@@ -199,7 +153,7 @@ def bounded_by_gradual[T: Any](t: T) -> None:
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: T) -> None:
def bounded_final[T: FinalClass](t: list[T]) -> None:
static_assert(is_assignable_to(T, FinalClass))
static_assert(not is_assignable_to(FinalClass, T))
@@ -213,14 +167,14 @@ true even if both typevars are bounded by the same final class, since you can sp
typevars to `Never` in addition to that final class.
```py
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
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: T, u: U) -> None:
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))
@@ -235,7 +189,7 @@ intersection of all of its constraints is a subtype of the typevar.
```py
from knot_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: T) -> None:
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))
@@ -260,7 +214,7 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
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: T) -> None:
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))
@@ -302,7 +256,7 @@ distinct constraints, meaning that there is (still) no guarantee that they will
the same type.
```py
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
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))
@@ -312,7 +266,7 @@ def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
@final
class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
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))
@@ -331,7 +285,7 @@ non-singleton type.
```py
from knot_extensions import is_singleton, is_single_valued, static_assert
def unbounded_unconstrained[T](t: T) -> None:
def unbounded_unconstrained[T](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
@@ -340,7 +294,7 @@ A bounded typevar is not a singleton, even if its bound is a singleton, since it
specialized to `Never`.
```py
def bounded[T: None](t: T) -> None:
def bounded[T: None](t: list[T]) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
@@ -351,14 +305,14 @@ specialize a constrained typevar to a subtype of a constraint.)
```py
from typing_extensions import Literal
def constrained_non_singletons[T: (int, str)](t: T) -> None:
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: T) -> None:
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: T) -> None:
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
static_assert(is_single_valued(T))
```
@@ -553,20 +507,6 @@ def remove_constraint[T: (int, str, bool)](t: T) -> None:
reveal_type(x) # revealed: T & Any
```
The intersection of a typevar with any other type is assignable to (and if fully static, a subtype
of) itself.
```py
from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not
def intersection_is_assignable[T](t: T) -> None:
static_assert(is_assignable_to(Intersection[T, None], T))
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
static_assert(is_subtype_of(Intersection[T, None], T))
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
```
## Narrowing
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:

View File

@@ -1,10 +1,5 @@
# Scoping rules for type variables
```toml
[environment]
python-version = "3.12"
```
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
spec.
@@ -64,8 +59,14 @@ to a different type each time.
def f[T](x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f("a")) # revealed: Literal["a"]
# TODO: no error
# TODO: revealed: int or Literal[1]
# error: [invalid-argument-type]
reveal_type(f(1)) # revealed: T
# TODO: no error
# TODO: revealed: str or Literal["a"]
# error: [invalid-argument-type]
reveal_type(f("a")) # revealed: T
```
## Methods can mention class typevars
@@ -137,12 +138,15 @@ from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")
# TODO: no error
# error: [invalid-base]
class Legacy(Generic[T]):
def m(self, x: T, y: S) -> S:
return y
legacy: Legacy[int] = Legacy()
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
# TODO: revealed: str
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions)
```
With PEP 695 syntax, it is clearer that the method uses a separate typevar:
@@ -153,7 +157,10 @@ class C[T]:
return y
c: C[int] = C()
reveal_type(c.m(1, "string")) # revealed: Literal["string"]
# TODO: no errors
# TODO: revealed: str
# error: [invalid-argument-type]
reveal_type(c.m(1, "string")) # revealed: S
```
## Unbound typevars
@@ -171,11 +178,13 @@ S = TypeVar("S")
def f(x: T) -> None:
x: list[T] = []
# TODO: invalid-assignment error
# TODO: error
y: list[S] = []
# TODO: no error
# error: [invalid-base]
class C(Generic[T]):
# TODO: error: cannot use S if it's not in the current generic context
# TODO: error
x: list[S] = []
# This is not an error, as shown in the previous test
@@ -195,11 +204,11 @@ S = TypeVar("S")
def f[T](x: T) -> None:
x: list[T] = []
# TODO: invalid assignment error
# TODO: error
y: list[S] = []
class C[T]:
# TODO: error: cannot use S if it's not in the current generic context
# TODO: error
x: list[S] = []
def m1(self, x: S) -> S:
@@ -254,7 +263,8 @@ def f[T](x: T, y: T) -> None:
class Ok[S]: ...
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: error for reuse of typevar
# TODO: no non-subscriptable error, error for reuse of typevar
# error: [non-subscriptable]
class Bad2(Iterable[T]): ...
```
@@ -267,7 +277,8 @@ class C[T]:
class Ok1[S]: ...
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: error for reuse of typevar
# TODO: no non-subscriptable error, error for reuse of typevar
# error: [non-subscriptable]
class Bad2(Iterable[T]): ...
```
@@ -281,7 +292,7 @@ class C[T]:
ok1: list[T] = []
class Bad:
# TODO: error: cannot refer to T in nested scope
# TODO: error
bad: list[T] = []
class Inner[S]: ...

View File

@@ -1,277 +0,0 @@
# Variance
```toml
[environment]
python-version = "3.12"
```
Type variables have a property called _variance_ that affects the subtyping and assignability
relations. Much more detail can be found in the [spec]. To summarize, each typevar is either
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
currently mentioned in the typing spec, but is a fourth case that we must consider.)
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar
`C[T]`, and two types `A` and `B`.
## Covariance
With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`.
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
get from the sequence is a valid `int`.
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
def receive(self) -> T:
raise ValueError
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A]))
static_assert(not is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
```
## Contravariance
With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`.
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
that you pass into the consumer is a valid `int`.
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
def send(self, value: T): ...
static_assert(not is_assignable_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not is_subtype_of(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A]))
static_assert(not is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
```
## Invariance
With an invariant typevar, _no_ specializations of the generic class are subtypes of each other.
This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
Iterating over the elements in a list would work with a covariant typevar, just like with the
"producer" type above. Appending elements to a list would work with a contravariant typevar, just
like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant
at the same time!
If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list
of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list
would no longer only contain elements that are subtypes of `bool`.
Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a
mutable list of `int`s, since you might try to extract elements from the list: you expect every
element that you extract to be a subtype of `bool`, but the list can contain any `int`.
In the end, if you expect a mutable list, you must always be given a list of exactly that type,
since we can't know in advance which of the allowed methods you'll want to use.
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
def send(self, value: T): ...
def receive(self) -> T:
raise ValueError
static_assert(not is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A]))
static_assert(not is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
```
## Bivariance
With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact,
equivalent to) each other.
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_
the typevar was used.)
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
pass
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[A], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[A], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[B], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
```
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance

View File

@@ -7,7 +7,7 @@ Builtin symbols can be explicitly imported:
```py
import builtins
reveal_type(builtins.chr) # revealed: def chr(i: SupportsIndex, /) -> str
reveal_type(builtins.chr) # revealed: def chr(i: int | SupportsIndex, /) -> str
```
## Implicit use of builtin
@@ -15,7 +15,7 @@ reveal_type(builtins.chr) # revealed: def chr(i: SupportsIndex, /) -> str
Or used implicitly:
```py
reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str
reveal_type(chr) # revealed: def chr(i: int | SupportsIndex, /) -> str
reveal_type(str) # revealed: Literal[str]
```

View File

@@ -122,11 +122,6 @@ from c import Y # error: [unresolved-import]
## Esoteric definitions and redefinintions
```toml
[environment]
python-version = "3.12"
```
We understand all public symbols defined in an external module as being imported by a `*` import,
not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section
provides tests for definitions, and redefinitions, that use more esoteric AST nodes.
@@ -189,7 +184,7 @@ match 42:
...
case [O]:
...
case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable"
case P | Q:
...
case object(foo=R):
...
@@ -289,7 +284,7 @@ match 42:
...
case [D]:
...
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
case E | F:
...
case object(foo=G):
...
@@ -357,7 +352,7 @@ match 42:
...
case [D]:
...
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
case E | F:
...
case object(foo=G):
...
@@ -631,30 +626,6 @@ reveal_type(X) # revealed: Unknown
reveal_type(Y) # revealed: bool
```
### An implicit import in a `.pyi` file later overridden by another assignment
`a.pyi`:
```pyi
X: bool = True
```
`b.pyi`:
```pyi
from a import X
X: bool = False
```
`c.py`:
```py
from b import *
reveal_type(X) # revealed: bool
```
## Visibility constraints
If an `importer` module contains a `from exporter import *` statement in its global namespace, the
@@ -894,10 +865,15 @@ from exporter import *
reveal_type(X) # revealed: bool
reveal_type(_private) # revealed: bool
reveal_type(__protected) # revealed: bool
reveal_type(__dunder__) # revealed: bool
reveal_type(___thunder___) # revealed: bool
# TODO none of these should error, should all reveal `bool`
# error: [unresolved-reference]
reveal_type(_private) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__protected) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__dunder__) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(___thunder___) # revealed: Unknown
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
reveal_type(Y) # revealed: bool
@@ -923,9 +899,6 @@ reveal_type(X) # revealed: bool
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
reveal_type(Y) # revealed: bool
# error: [unresolved-reference]
reveal_type(__all__) # revealed: Unknown
```
### `__all__` with additions later on in the global scope
@@ -1099,44 +1072,6 @@ reveal_type(Y) # revealed: bool
reveal_type(Z) # revealed: Unknown
```
### `__all__` conditionally defined in a statically known branch (2)
The same example again, but with a different `python-version` set:
```toml
[environment]
python-version = "3.10"
```
`exporter.py`:
```py
import sys
X: bool = True
if sys.version_info >= (3, 11):
__all__ = ["X", "Y"]
Y: bool = True
else:
__all__ = ("Z",)
Z: bool = True
```
`importer.py`:
```py
from exporter import *
# TODO: should reveal `Unknown` and emit `[unresolved-reference]`
reveal_type(X) # revealed: bool
# error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
reveal_type(Z) # revealed: bool
```
### `__all__` conditionally mutated in a statically known branch
```toml
@@ -1149,11 +1084,11 @@ python-version = "3.11"
```py
import sys
__all__ = []
__all__ = ["X"]
X: bool = True
if sys.version_info >= (3, 11):
__all__.extend(["X", "Y"])
__all__.append("Y")
Y: bool = True
else:
__all__.append("Z")
@@ -1172,45 +1107,6 @@ reveal_type(Y) # revealed: bool
reveal_type(Z) # revealed: Unknown
```
### `__all__` conditionally mutated in a statically known branch (2)
The same example again, but with a different `python-version` set:
```toml
[environment]
python-version = "3.10"
```
`exporter.py`:
```py
import sys
__all__ = []
X: bool = True
if sys.version_info >= (3, 11):
__all__.extend(["X", "Y"])
Y: bool = True
else:
__all__.append("Z")
Z: bool = True
```
`importer.py`:
```py
from exporter import *
# TODO: should reveal `Unknown` & emit `[unresolved-reference]
reveal_type(X) # revealed: bool
# error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
reveal_type(Z) # revealed: bool
```
### Empty `__all__`
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
@@ -1270,7 +1166,6 @@ from b import *
# TODO: should not error, should reveal `bool`
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
# See https://github.com/astral-sh/ruff/issues/16159
#
# error: [unresolved-reference]
reveal_type(X) # revealed: Unknown

View File

@@ -191,9 +191,9 @@ def _(
i2: Intersection[P | Q | R, S],
i3: Intersection[P | Q, R | S],
) -> None:
reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S)
reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S)
reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S)
reveal_type(i1) # revealed: P & Q | P & R | P & S
reveal_type(i2) # revealed: P & S | Q & S | R & S
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
@@ -216,7 +216,7 @@ def simplifications_for_same_elements(
# = P & Q | P & R | Q | Q & R
# = Q | P & R
# (again, because Q is a supertype of P & Q and of Q & R)
reveal_type(i3) # revealed: Q | (P & R)
reveal_type(i3) # revealed: Q | P & R
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q
@@ -842,7 +842,7 @@ def unknown(
### Mixed dynamic types
Gradually-equivalent types can be simplified out of intersections:
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
```py
from typing import Any
@@ -854,10 +854,10 @@ def mixed(
i3: Intersection[Not[Any], Unknown],
i4: Intersection[Not[Any], Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: Any
reveal_type(i3) # revealed: Any
reveal_type(i4) # revealed: Any
reveal_type(i1) # revealed: Any & Unknown
reveal_type(i2) # revealed: Any & Unknown
reveal_type(i3) # revealed: Any & Unknown
reveal_type(i4) # revealed: Any & Unknown
```
## Invalid

View File

@@ -286,7 +286,7 @@ class Test:
def __iter__(self) -> TestIter | int:
return TestIter()
# error: [not-iterable] "Object of type `Test` may not be iterable"
# error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method"
for x in Test():
reveal_type(x) # revealed: int
```
@@ -316,12 +316,12 @@ def _(flag: bool):
else:
__iter__: None = None
# error: [not-iterable] "Object of type `Iterable1` may not be iterable"
# error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable"
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable] "Object of type `Iterable2` may not be iterable"
# error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable"
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
@@ -376,7 +376,7 @@ def _(flag: bool):
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable] "Object of type `Iterable` may not be iterable"
# error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method"
for x in Iterable():
reveal_type(x) # revealed: int
```
@@ -461,7 +461,7 @@ def _(flag: bool):
return Iterator()
__getitem__: None = None
# error: [not-iterable] "Object of type `Iterable` may not be iterable"
# error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable"
for x in Iterable():
reveal_type(x) # revealed: int
```

View File

@@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
while NotBoolable():
...
```

View File

@@ -22,7 +22,6 @@ We can then place custom stub files in `/typeshed/stdlib`, for example:
`/typeshed/stdlib/builtins.pyi`:
```pyi
class object: ...
class BuiltinClass: ...
builtin_symbol: BuiltinClass

View File

@@ -53,25 +53,6 @@ class B(A): ...
reveal_type(B.__class__) # revealed: Literal[M]
```
## Linear inheritance with PEP 695 generic class
The same is true if the base with the metaclass is a generic class.
```toml
[environment]
python-version = "3.13"
```
```py
class M(type): ...
class A[T](metaclass=M): ...
class B(A): ...
class C(A[int]): ...
reveal_type(B.__class__) # revealed: Literal[M]
reveal_type(C.__class__) # revealed: Literal[M]
```
## Conflict (1)
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
@@ -235,11 +216,6 @@ reveal_type(A.__class__) # revealed: type[Unknown]
## PEP 695 generic
```toml
[environment]
python-version = "3.12"
```
```py
class M(type): ...
class A[T: str](metaclass=M): ...

Some files were not shown because too many files have changed in this diff Show More