Compare commits

..

3 Commits

Author SHA1 Message Date
Douglas Creager
7c976dc570 Merge branch 'main' into dcreager/function-enum
* main:
  Update pre-commit dependencies (#17506)
  [red-knot] Simplify visibility constraint handling for `*`-import definitions (#17486)
  [red-knot] Detect (some) invalid protocols (#17488)
  [red-knot] Correctly identify protocol classes (#17487)
  Update dependency ruff to v0.11.6 (#17516)
  Update Rust crate shellexpand to v3.1.1 (#17512)
  Update Rust crate proc-macro2 to v1.0.95 (#17510)
  Update Rust crate rand to v0.9.1 (#17511)
  Update Rust crate libc to v0.2.172 (#17509)
  Update Rust crate jiff to v0.2.9 (#17508)
  Update Rust crate clap to v4.5.37 (#17507)
  Update astral-sh/setup-uv action to v5.4.2 (#17504)
  Update taiki-e/install-action digest to 09dc018 (#17503)
  [red-knot] infer attribute assignments bound in comprehensions (#17396)
  [red-knot] simplify gradually-equivalent types out of unions and intersections (#17467)
  [red-knot] pull primer projects to run from file (#17473)
2025-04-21 13:18:36 -04:00
Douglas Creager
b44fb47f25 create generic context lazily 2025-04-21 13:15:09 -04:00
Douglas Creager
0a4dec0323 start pulling out enum 2025-04-18 17:04:26 -04:00
1015 changed files with 75912 additions and 10663 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

@@ -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

@@ -239,11 +239,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-insta
- name: Red-knot mdtests (GitHub annotations)
@@ -293,11 +293,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -320,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@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -346,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"
@@ -403,11 +403,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -821,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"
@@ -857,7 +857,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-codspeed

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
@@ -46,7 +45,7 @@ jobs:
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/hauntsaninja/mypy_primer@4c22d192a456e27badf85b3ea0f830707375d2b7"
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
- name: Run mypy_primer
shell: bash

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

@@ -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.6
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,30 +1,5 @@
# Changelog
## 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

125
Cargo.lock generated
View File

@@ -394,7 +394,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -490,6 +490,20 @@ dependencies = [
"windows-sys 0.52.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]]
name = "compact_str"
version = "0.9.0"
@@ -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,9 +1553,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.10"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -1549,18 +1563,18 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "jiff-static"
version = "0.2.10"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -1657,7 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa96ed35d0dccc67cf7ba49350cb86de3dcb1d072a7ab28f99117f19d874953"
dependencies = [
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -2166,7 +2180,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -2235,7 +2249,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -2558,7 +2572,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"camino",
"compact_str",
"compact_str 0.9.0",
"countme",
"dir-test",
"drop_bomb",
@@ -2647,7 +2661,6 @@ dependencies = [
"tempfile",
"thiserror 2.0.12",
"toml",
"tracing",
]
[[package]]
@@ -2759,7 +2772,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.7"
version = "0.11.6"
dependencies = [
"anyhow",
"argfile",
@@ -2994,7 +3007,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.7"
version = "0.11.6"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3061,7 +3074,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3088,7 +3101,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 +3199,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"bstr",
"compact_str",
"compact_str 0.9.0",
"insta",
"memchr",
"ruff_annotate_snippets",
@@ -3320,7 +3333,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.7"
version = "0.11.6"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3444,11 +3457,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.20.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1"
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 +3480,18 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.20.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
[[package]]
name = "salsa-macros"
version = "0.20.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1"
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 +3525,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3561,7 +3574,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3572,7 +3585,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3595,7 +3608,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3636,7 +3649,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3767,7 +3780,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3783,9 +3796,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 +3813,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3871,7 +3884,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3882,7 +3895,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"test-case-core",
]
@@ -3918,7 +3931,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -3929,7 +3942,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4060,7 +4073,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4327,7 +4340,7 @@ checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4449,7 +4462,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"wasm-bindgen-shared",
]
@@ -4484,7 +4497,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4519,7 +4532,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4634,7 +4647,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4645,7 +4658,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4883,7 +4896,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"synstructure",
]
@@ -4904,7 +4917,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]
@@ -4924,7 +4937,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
"synstructure",
]
@@ -4947,7 +4960,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"syn 2.0.100",
]
[[package]]

View File

@@ -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 = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" }
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"] }
@@ -231,10 +231,6 @@ unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
# Salsa generates functions with parameters for each field of a `salsa::interned` struct.
# If we don't allow this, we get warnings for structs with too many fields.
too_many_arguments = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
@@ -276,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
@@ -312,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
@@ -340,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.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.6/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.7
rev: v0.11.6
hooks:
# Run the linter.
- id: ruff

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]).
@@ -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

@@ -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

@@ -5,94 +5,6 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
#[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
--> <temp_dir>/.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
--> <temp_dir>/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
--> <temp_dir>/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
--> <temp_dir>/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]
@@ -120,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`
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
@@ -253,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`
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)
|
@@ -353,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
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)):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
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
@@ -389,11 +301,11 @@ 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
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)):
|
@@ -429,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`
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
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)):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
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
@@ -476,22 +388,22 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-import: Cannot resolve import `does_not_exit`
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
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)):
|
@@ -527,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
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)):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
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
@@ -564,11 +476,11 @@ 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
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)):
|
@@ -643,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
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
|
Found 1 diagnostic
@@ -726,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
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
|
Found 1 diagnostic
@@ -758,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
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
| ^ Name `x` used when not defined
|
Found 1 diagnostic
@@ -787,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
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
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
@@ -825,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
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
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
@@ -863,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
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
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
@@ -923,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
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)):
|
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
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
@@ -965,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
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)):
|
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
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
@@ -1023,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
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^
|
error: lint:unresolved-import: Cannot resolve import `main2`
--> <temp_dir>/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`
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
@@ -1060,20 +972,20 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import: Cannot resolve import `main2`
--> <temp_dir>/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`
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

View File

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

View File

@@ -11,7 +11,7 @@ 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,
DiagnosticId, Severity, Span,
};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
@@ -20,10 +20,8 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
use rustc_hash::FxHashSet;
use salsa::Durability;
use salsa::Setter;
use std::panic::{catch_unwind, AssertUnwindSafe, UnwindSafe};
use std::sync::Arc;
use thiserror::Error;
use tracing::error;
pub mod combine;
@@ -189,66 +187,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> {
@@ -352,23 +314,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!(
@@ -473,16 +432,7 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
.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
@@ -573,45 +523,6 @@ 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 catch_unwind(|| {
// Ignore salsa errors
salsa::Cancelled::catch(f).ok()
}) {
Ok(result) => Ok(result),
Err(error) => {
let payload = if let Some(s) = error.downcast_ref::<&str>() {
Some((*s).to_string())
} else {
error.downcast_ref::<String>().cloned()
};
let message = if let Some(payload) = payload {
format!(
"Panicked while checking `{file}`: `{payload}`",
file = file.path(db)
)
} else {
format!("Panicked while checking `{file}`", file = { file.path(db) })
};
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));
Err(diagnostic)
}
}
}
#[cfg(test)]
mod tests {
use crate::db::tests::TestDb;

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 {
@@ -136,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

@@ -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

@@ -50,9 +50,10 @@ y: Any = "not an Any" # error: [invalid-assignment]
The spec allows you to define subclasses of `Any`.
`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`.
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
@@ -62,33 +63,13 @@ class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: Subclass = 1 # error: [invalid-assignment]
y: int = Subclass()
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
be a subclass of `FinalClass`:
```py
from typing import final
@final
class FinalClass: ...
f: FinalClass = Subclass() # error: [invalid-assignment]
```
A use case where this comes up is with mocking libraries, where the mock object should be assignable
to any type:
```py
from unittest.mock import MagicMock
x: int = MagicMock()
```
## Invalid
`Any` cannot be parameterized:

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(unknown type subscript)
```
## Invalid Collection based AST nodes

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

View File

@@ -106,13 +106,13 @@ 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): ...
@@ -124,6 +124,6 @@ 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

@@ -89,12 +89,9 @@ 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

@@ -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

@@ -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

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

@@ -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

@@ -292,7 +292,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

@@ -689,7 +689,7 @@ from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
@dataclass_with_order
class C:

View File

@@ -1,165 +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())
```
## `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

@@ -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

@@ -232,11 +232,21 @@ TODO: These do not currently work yet, because we don't correctly model the nest
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]]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, 1)) # revealed: C[Unknown]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, "string")) # revealed: C[Unknown]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, True)) # revealed: C[Unknown]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`"
wrong_innards: C[int] = C("five", 1)
```
@@ -275,17 +285,15 @@ c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
```
## 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 +301,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 +312,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 +323,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

@@ -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

@@ -189,7 +189,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 +289,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 +357,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):
...

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

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

View File

@@ -191,8 +191,8 @@ reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Lit
## `__bases__` includes a `Union`
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
find a union type in a class's bases, we infer the class's `__mro__` as being
We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`.
If we find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py

View File

@@ -29,7 +29,7 @@ def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
assert x is 2
reveal_type(x) # revealed: Literal[2]
assert y == 2
reveal_type(y) # revealed: Literal[2]
reveal_type(y) # revealed: Literal[1, 2, 3]
```
## `assert` with `isinstance`

View File

@@ -10,7 +10,7 @@ def _(x: A | B):
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: (B & ~A) | (A & ~B)
reveal_type(x) # revealed: B & ~A | A & ~B
```
## Arms might not add narrowing constraints
@@ -131,8 +131,8 @@ def _(x: A | B | C, y: A | B | C):
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
reveal_type(y) # revealed: (B & ~A) | (C & ~A)
reveal_type(x) # revealed: B & ~A | C & ~A
reveal_type(y) # revealed: B & ~A | C & ~A
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
@@ -155,7 +155,7 @@ def _(x: A | B | C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: (A & ~B) | C
reveal_type(x) # revealed: A & ~B | C
```
## mixing `or` and `not`
@@ -167,7 +167,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | (A & ~C)
reveal_type(x) # revealed: B | A & ~C
else:
reveal_type(x) # revealed: C & ~B
```
@@ -181,7 +181,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | (B & ~C)
reveal_type(x) # revealed: A | B & ~C
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
@@ -197,7 +197,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: (A & B) | (A & ~C)
reveal_type(x) # revealed: A & B | A & ~C
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
@@ -206,7 +206,7 @@ def _(x: A | B | C):
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B)
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
```
## Boolean expression internal narrowing

View File

@@ -20,9 +20,11 @@ def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x == 1:
reveal_type(x) # revealed: Literal[1]
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x == 2:
reveal_type(x) # revealed: Literal[2]
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[3]
```
@@ -36,11 +38,14 @@ def _(flag1: bool, flag2: bool):
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x != 2:
reveal_type(x) # revealed: Literal[1]
# TODO should be `Literal[1]`
reveal_type(x) # revealed: Literal[1, 3]
elif x == 3:
reveal_type(x) # revealed: Never
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
else:
reveal_type(x) # revealed: Never
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
```
## Assignment expressions

View File

@@ -31,14 +31,17 @@ def _(flag1: bool, flag2: bool):
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
reveal_type(x) # revealed: Literal[2]
# TODO should be `Literal[2]`
reveal_type(x) # revealed: Literal[2, 3]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
elif x != 2:
reveal_type(x) # revealed: Literal[1]
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 3]
else:
reveal_type(x) # revealed: Never
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
```

View File

@@ -9,7 +9,8 @@ def _(flag: bool):
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
```
## `!=` for other singleton types
@@ -21,7 +22,8 @@ def _(flag: bool):
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
```
## `x != y` where `y` is of literal type
@@ -45,7 +47,8 @@ def _(flag: bool):
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
reveal_type(C) # revealed: Literal[A]
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
```
## `x != y` where `y` has multiple single-valued options
@@ -58,7 +61,8 @@ def _(flag1: bool, flag2: bool):
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[2]
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
```
## `!=` for non-single-valued types
@@ -97,61 +101,6 @@ def f() -> Literal[1, 2, 3]:
if (x := f()) != 1:
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[1]
```
## Union with `Any`
```py
from typing import Any
def _(x: Any | None, y: Any | None):
if x != 1:
reveal_type(x) # revealed: (Any & ~Literal[1]) | None
if y == 1:
reveal_type(y) # revealed: Any & ~None
```
## Booleans and integers
```py
from typing import Literal
def _(b: bool, i: Literal[1, 2]):
if b == 1:
reveal_type(b) # revealed: Literal[True]
else:
reveal_type(b) # revealed: Literal[False]
if b == 6:
reveal_type(b) # revealed: Never
else:
reveal_type(b) # revealed: bool
if b == 0:
reveal_type(b) # revealed: Literal[False]
else:
reveal_type(b) # revealed: Literal[True]
if i == True:
reveal_type(i) # revealed: Literal[1]
else:
reveal_type(i) # revealed: Literal[2]
```
## Narrowing `LiteralString` in union
```py
from typing_extensions import Literal, LiteralString, Any
def _(s: LiteralString | None, t: LiteralString | Any):
if s == "foo":
reveal_type(s) # revealed: Literal["foo"]
if s == 1:
reveal_type(s) # revealed: Never
if t == "foo":
# TODO could be `Literal["foo"] | Any`
reveal_type(t) # revealed: LiteralString | Any
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
```

View File

@@ -82,19 +82,19 @@ class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
else:
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
if x and not x:
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
else:
reveal_type(x) # revealed: A | B
if x or not x:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
```
### Truthiness of Types
@@ -111,9 +111,9 @@ x = int if flag() else str
reveal_type(x) # revealed: Literal[int, str]
if x:
reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy)
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
else:
reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy)
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
```
## Determined Truthiness
@@ -176,12 +176,12 @@ if isinstance(x, str) and not isinstance(x, B):
z = x if flag() else y
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
if z:
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
else:
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
```
## Narrowing Multiple Variables
@@ -264,13 +264,13 @@ def _(
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
@@ -296,12 +296,12 @@ def _(x: Literal[0, 1]):
reveal_type(x and A()) # revealed: Literal[0] | A
def _(x: str):
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
def _(x: bool | str):
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
class Falsy:
def __bool__(self) -> Literal[False]:

View File

@@ -127,7 +127,7 @@ class B: ...
def _[T](x: A | B):
if type(x) is A[str]:
reveal_type(x) # revealed: (A[int] & A[Unknown]) | (B & A[Unknown])
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
else:
reveal_type(x) # revealed: A[int] | B
```

View File

@@ -1,25 +0,0 @@
# repro interned panic
## before
```toml
log = "salsa=trace,red_knot_test,ruff_db=trace,red_knot_ide=trace,red_knot_project=trace"
[environment]
python-version = "3.9"
```
```py
None
```
## after
```toml
log = "salsa=trace,red_knot_test,ruff_db=trace,red_knot_ide=trace,red_knot_project=trace"
[environment]
python-version = "3.10"
```
```py
None
```

View File

@@ -230,7 +230,7 @@ And it is also an error to use `Protocol` in type expressions:
def f(
x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too
):
) -> None:
reveal_type(x) # revealed: Unknown
# TODO: should be `type[Unknown]`
@@ -242,7 +242,7 @@ def f(
Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
```py
# Could also be `Literal[True]`, but `bool` is fine:
# TODO: should be `Literal[True]`
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
```
@@ -266,7 +266,9 @@ class Bar(typing_extensions.Protocol):
static_assert(typing_extensions.is_protocol(Foo))
static_assert(typing_extensions.is_protocol(Bar))
static_assert(is_equivalent_to(Foo, Bar))
# TODO: should pass
static_assert(is_equivalent_to(Foo, Bar)) # error: [static-assert-error]
```
The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`:
@@ -282,7 +284,9 @@ class RuntimeCheckableBar(typing_extensions.Protocol):
static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo))
static_assert(typing_extensions.is_protocol(RuntimeCheckableBar))
static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar))
# TODO: should pass
static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # error: [static-assert-error]
# These should not error because the protocols are decorated with `@runtime_checkable`
isinstance(object(), RuntimeCheckableFoo)
@@ -300,12 +304,10 @@ reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool
## Calls to protocol classes
<!-- snapshot-diagnostics -->
Neither `Protocol`, nor any protocol class, can be directly instantiated:
```py
from typing_extensions import Protocol, reveal_type
from typing import Protocol
# error: [call-non-callable]
reveal_type(Protocol()) # revealed: Unknown
@@ -313,7 +315,7 @@ reveal_type(Protocol()) # revealed: Unknown
class MyProtocol(Protocol):
x: int
# error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
# error
reveal_type(MyProtocol()) # revealed: MyProtocol
```
@@ -361,8 +363,25 @@ class Foo(Protocol):
def method_member(self) -> bytes:
return b"foo"
# TODO: actually a frozenset (requires support for legacy generics)
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["method_member"], Literal["x"], Literal["y"], Literal["z"]]
# TODO: at runtime, `get_protocol_members` returns a `frozenset`,
# but for now we might pretend it returns a `tuple`, as we support heterogeneous `tuple` types
# but not yet generic `frozenset`s
#
# So this should either be
#
# `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]`
#
# `frozenset[Literal["x", "y", "z", "method_member"]]`
reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class)
```
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
```py
class NotAProtocol: ...
# TODO: should emit `[invalid-protocol]` error, should reveal `Unknown`
reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class)
```
Certain special attributes and methods are not considered protocol members at runtime, and should
@@ -380,87 +399,8 @@ class Lumberjack(Protocol):
def __init__(self, x: int) -> None:
self.x = x
# TODO: actually a frozenset
reveal_type(get_protocol_members(Lumberjack)) # revealed: tuple[Literal["x"]]
```
A sub-protocol inherits and extends the members of its superclass protocol(s):
```py
class Bar(Protocol):
spam: str
class Baz(Bar, Protocol):
ham: memoryview
# TODO: actually a frozenset
reveal_type(get_protocol_members(Baz)) # revealed: tuple[Literal["ham"], Literal["spam"]]
class Baz2(Bar, Foo, Protocol): ...
# TODO: actually a frozenset
# revealed: tuple[Literal["method_member"], Literal["spam"], Literal["x"], Literal["y"], Literal["z"]]
reveal_type(get_protocol_members(Baz2))
```
## Protocol members in statically known branches
The list of protocol members does not include any members declared in branches that are statically
known to be unreachable:
```toml
[environment]
python-version = "3.9"
```
```py
import sys
from typing_extensions import Protocol, get_protocol_members
class Foo(Protocol):
if sys.version_info >= (3, 10):
a: int
b = 42
def c(self) -> None: ...
else:
d: int
e = 56
def f(self) -> None: ...
# TODO: actually a frozenset
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["d"], Literal["e"], Literal["f"]]
```
## Invalid calls to `get_protocol_members()`
<!-- snapshot-diagnostics -->
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Protocol, get_protocol_members
class NotAProtocol: ...
get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
class AlsoNotAProtocol(NotAProtocol, object): ...
get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
```
The original class object must be passed to the function; a specialised version of a generic version
does not suffice:
```py
class GenericProtocol[T](Protocol): ...
get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
# TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]`
reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class)
```
## Subtyping of protocols with attribute members
@@ -469,11 +409,6 @@ In the following example, the protocol class `HasX` defines an interface such th
static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a
mutable `x` attribute of type `int`:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol
from knot_extensions import static_assert, is_assignable_to, is_subtype_of
@@ -484,20 +419,21 @@ class HasX(Protocol):
class Foo:
x: int
static_assert(is_subtype_of(Foo, HasX))
static_assert(is_assignable_to(Foo, HasX))
# TODO: these should pass
static_assert(is_subtype_of(Foo, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(Foo, HasX)) # error: [static-assert-error]
class FooSub(Foo): ...
static_assert(is_subtype_of(FooSub, HasX))
static_assert(is_assignable_to(FooSub, HasX))
# TODO: these should pass
static_assert(is_subtype_of(FooSub, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(FooSub, HasX)) # error: [static-assert-error]
class Bar:
x: str
# TODO: these should pass
static_assert(not is_subtype_of(Bar, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(Bar, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(Bar, HasX))
static_assert(not is_assignable_to(Bar, HasX))
class Baz:
y: int
@@ -519,16 +455,14 @@ class A:
def x(self) -> int:
return 42
# TODO: these should pass
static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(A, HasX))
static_assert(not is_assignable_to(A, HasX))
class B:
x: Final = 42
# TODO: these should pass
static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(A, HasX))
static_assert(not is_assignable_to(A, HasX))
class IntSub(int): ...
@@ -538,10 +472,8 @@ class C:
# due to invariance, a type is only a subtype of `HasX`
# if its `x` attribute is of type *exactly* `int`:
# a subclass of `int` does not satisfy the interface
#
# TODO: these should pass
static_assert(not is_subtype_of(C, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(C, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(C, HasX))
static_assert(not is_assignable_to(C, HasX))
```
All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes
@@ -555,23 +487,22 @@ from typing import NamedTuple
class MutableDataclass:
x: int
static_assert(is_subtype_of(MutableDataclass, HasX))
static_assert(is_assignable_to(MutableDataclass, HasX))
# TODO: these should pass
static_assert(is_subtype_of(MutableDataclass, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(MutableDataclass, HasX)) # error: [static-assert-error]
@dataclass(frozen=True)
class ImmutableDataclass:
x: int
# TODO: these should pass
static_assert(not is_subtype_of(ImmutableDataclass, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(ImmutableDataclass, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(ImmutableDataclass, HasX))
static_assert(not is_assignable_to(ImmutableDataclass, HasX))
class NamedTupleWithX(NamedTuple):
x: int
# TODO: these should pass
static_assert(not is_subtype_of(NamedTupleWithX, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(NamedTupleWithX, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(NamedTupleWithX, HasX))
static_assert(not is_assignable_to(NamedTupleWithX, HasX))
```
However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX`
@@ -590,8 +521,9 @@ class XProperty:
def x(self, x: int) -> None:
self._x = x**2
static_assert(is_subtype_of(XProperty, HasX))
static_assert(is_assignable_to(XProperty, HasX))
# TODO: these should pass
static_assert(is_subtype_of(XProperty, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(XProperty, HasX)) # error: [static-assert-error]
```
Attribute members on protocol classes are allowed to have default values, just like instance
@@ -616,61 +548,9 @@ def f(arg: HasXWithDefault):
reveal_type(type(arg).x) # revealed: int
```
Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot unless the
symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is
stricter validation of protocol members than many other type checkers currently apply (as of
2025/04/21).
The reason for this strict validation is that undeclared variables in the class body would lead to
an ambiguous interface being declared by the protocol.
```py
from typing_extensions import TypeAlias, get_protocol_members
class MyContext:
def __enter__(self) -> int:
return 42
def __exit__(self, *args) -> None: ...
class LotsOfBindings(Protocol):
a: int
a = 42 # this is fine, since `a` is declared in the class body
b: int = 56 # this is also fine, by the same principle
type c = str # this is very strange but I can't see a good reason to disallow it
d: TypeAlias = bytes # same here
class Nested: ... # also weird, but we should also probably allow it
class NestedProtocol(Protocol): ... # same here...
e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared)
f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared)
h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared)
for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared)
pass
with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared)
pass
match object():
case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared)
...
# TODO: actually a frozenset
# revealed: tuple[Literal["Nested"], Literal["NestedProtocol"], Literal["a"], Literal["b"], Literal["c"], Literal["d"], Literal["e"], Literal["f"], Literal["g"], Literal["h"], Literal["i"], Literal["j"], Literal["k"], Literal["l"]]
reveal_type(get_protocol_members(LotsOfBindings))
```
Attribute members are allowed to have assignments in methods on the protocol class, just like
non-protocol classes. Unlike other classes, however, instance attributes that are not declared in
the class body are disallowed. This is mandated by [the spec][spec_protocol_members]:
> Additional attributes *only* defined in the body of a method by assignment via `self` are not
> allowed. The rationale for this is that the protocol class implementation is often not shared by
> subtypes, so the interface should not depend on the default implementation.
non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that
are not declared in the class body -- are not allowed:
```py
class Foo(Protocol):
@@ -679,33 +559,11 @@ class Foo(Protocol):
def __init__(self) -> None:
self.x = 42 # fine
self.a = 56 # TODO: should emit diagnostic
self.b: int = 128 # TODO: should emit diagnostic
self.a = 56 # error
def non_init_method(self) -> None:
self.y = 64 # fine
self.c = 72 # TODO: should emit diagnostic
# Note: the list of members does not include `a`, `b` or `c`,
# as none of these attributes is declared in the class body.
#
# TODO: actually a frozenset
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["non_init_method"], Literal["x"], Literal["y"]]
```
If a member is declared in a superclass of a protocol class, it is fine for it to be assigned to in
the sub-protocol class without a redeclaration:
```py
class Super(Protocol):
x: int
class Sub(Super, Protocol):
x = 42 # no error here, since it's declared in the superclass
# TODO: actually frozensets
reveal_type(get_protocol_members(Super)) # revealed: tuple[Literal["x"]]
reveal_type(get_protocol_members(Sub)) # revealed: tuple[Literal["x"]]
self.b = 72 # error
```
If a protocol has 0 members, then all other types are assignable to it, and all fully static types
@@ -716,8 +574,9 @@ from typing import Protocol
class UniversalSet(Protocol): ...
static_assert(is_assignable_to(object, UniversalSet))
static_assert(is_subtype_of(object, UniversalSet))
# TODO: these should pass
static_assert(is_assignable_to(object, UniversalSet)) # error: [static-assert-error]
static_assert(is_subtype_of(object, UniversalSet)) # error: [static-assert-error]
```
Which means that `UniversalSet` here is in fact an equivalent type to `object`:
@@ -725,7 +584,8 @@ Which means that `UniversalSet` here is in fact an equivalent type to `object`:
```py
from knot_extensions import is_equivalent_to
static_assert(is_equivalent_to(UniversalSet, object))
# TODO: this should pass
static_assert(is_equivalent_to(UniversalSet, object)) # error: [static-assert-error]
```
`object` is a subtype of certain other protocols too. Since all fully static types (whether nominal
@@ -736,16 +596,17 @@ means that these protocols are also equivalent to `UniversalSet` and `object`:
class SupportsStr(Protocol):
def __str__(self) -> str: ...
static_assert(is_equivalent_to(SupportsStr, UniversalSet))
static_assert(is_equivalent_to(SupportsStr, object))
# TODO: these should pass
static_assert(is_equivalent_to(SupportsStr, UniversalSet)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsStr, object)) # error: [static-assert-error]
class SupportsClass(Protocol):
@property
def __class__(self) -> type: ...
__class__: type
static_assert(is_equivalent_to(SupportsClass, UniversalSet))
static_assert(is_equivalent_to(SupportsClass, SupportsStr))
static_assert(is_equivalent_to(SupportsClass, object))
# TODO: these should pass
static_assert(is_equivalent_to(SupportsClass, UniversalSet)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsClass, SupportsStr)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsClass, object)) # error: [static-assert-error]
```
If a protocol contains members that are not defined on `object`, then that protocol will (like all
@@ -782,7 +643,8 @@ class HasX(Protocol):
class AlsoHasX(Protocol):
x: int
static_assert(is_equivalent_to(HasX, AlsoHasX))
# TODO: this should pass
static_assert(is_equivalent_to(HasX, AlsoHasX)) # error: [static-assert-error]
```
And unions containing equivalent protocols are recognised as equivalent, even when the order is not
@@ -798,7 +660,8 @@ class AlsoHasY(Protocol):
class A: ...
class B: ...
static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A))
# TODO: this should pass
static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) # error: [static-assert-error]
```
## Intersections of protocols
@@ -876,9 +739,9 @@ from knot_extensions import is_subtype_of, is_assignable_to, static_assert, Type
class HasX(Protocol):
x: int
# TODO: this should pass
# TODO: these should pass
static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(TypeOf[module], HasX))
static_assert(is_assignable_to(TypeOf[module], HasX)) # error: [static-assert-error]
class ExplicitProtocolSubtype(HasX, Protocol):
y: int
@@ -890,8 +753,9 @@ class ImplicitProtocolSubtype(Protocol):
x: int
y: str
static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX))
static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX))
# TODO: these should pass
static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error]
class Meta(type):
x: int
@@ -926,24 +790,23 @@ def f(obj: ClassVarXProto):
class InstanceAttrX:
x: int
# TODO: these should pass
static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error]
static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error]
static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto))
static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto))
class PropertyX:
@property
def x(self) -> int:
return 42
# TODO: these should pass
static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) # error: [static-assert-error]
static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) # error: [static-assert-error]
static_assert(not is_assignable_to(PropertyX, ClassVarXProto))
static_assert(not is_subtype_of(PropertyX, ClassVarXProto))
class ClassVarX:
x: ClassVar[int] = 42
static_assert(is_assignable_to(ClassVarX, ClassVarXProto))
static_assert(is_subtype_of(ClassVarX, ClassVarXProto))
# TODO: these should pass
static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) # error: [static-assert-error]
static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) # error: [static-assert-error]
```
This is mentioned by the
@@ -970,16 +833,18 @@ class HasXProperty(Protocol):
class XAttr:
x: int
static_assert(is_subtype_of(XAttr, HasXProperty))
static_assert(is_assignable_to(XAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error]
class XReadProperty:
@property
def x(self) -> int:
return 42
static_assert(is_subtype_of(XReadProperty, HasXProperty))
static_assert(is_assignable_to(XReadProperty, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error]
class XReadWriteProperty:
@property
@@ -989,20 +854,22 @@ class XReadWriteProperty:
@x.setter
def x(self, val: int) -> None: ...
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
class XClassVar:
x: ClassVar[int] = 42
static_assert(is_subtype_of(XClassVar, HasXProperty))
static_assert(is_assignable_to(XClassVar, HasXProperty))
static_assert(is_subtype_of(XClassVar, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XClassVar, HasXProperty)) # error: [static-assert-error]
class XFinal:
x: Final = 42
static_assert(is_subtype_of(XFinal, HasXProperty))
static_assert(is_assignable_to(XFinal, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XFinal, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XFinal, HasXProperty)) # error: [static-assert-error]
```
A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below
@@ -1015,8 +882,9 @@ class MyInt(int): ...
class XSub:
x: MyInt
static_assert(is_subtype_of(XSub, HasXProperty))
static_assert(is_assignable_to(XSub, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error]
```
A read/write property on a protocol, where the getter returns the same type that the setter takes,
@@ -1032,17 +900,17 @@ class HasMutableXProperty(Protocol):
class XAttr:
x: int
static_assert(is_subtype_of(XAttr, HasXProperty))
static_assert(is_assignable_to(XAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error]
class XReadProperty:
@property
def x(self) -> int:
return 42
# TODO: these should pass
static_assert(not is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(not is_subtype_of(XReadProperty, HasXProperty))
static_assert(not is_assignable_to(XReadProperty, HasXProperty))
class XReadWriteProperty:
@property
@@ -1052,15 +920,15 @@ class XReadWriteProperty:
@x.setter
def x(self, val: int) -> None: ...
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
class XSub:
x: MyInt
# TODO: should pass
static_assert(not is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(not is_subtype_of(XSub, HasXProperty))
static_assert(not is_assignable_to(XSub, HasXProperty))
```
A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable
@@ -1072,13 +940,16 @@ from knot_extensions import is_equivalent_to
class HasMutableXAttr(Protocol):
x: int
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))
# TODO: this should pass
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasMutableXProperty, HasXProperty))
static_assert(is_assignable_to(HasMutableXProperty, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasMutableXProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) # error: [static-assert-error]
```
A read/write property on a protocol, where the setter accepts a subtype of the type returned by the
@@ -1105,8 +976,9 @@ class HasAsymmetricXProperty(Protocol):
class XAttr:
x: int
static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty))
static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error]
```
The end conclusion of this is that the getter-returned type of a property is always covariant and
@@ -1117,8 +989,9 @@ regular mutable attribute, where the implied getter-returned and setter-accepted
class XAttrSub:
x: MyInt
static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty))
static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error]
class MyIntSub(MyInt):
pass
@@ -1126,9 +999,8 @@ class MyIntSub(MyInt):
class XAttrSubSub:
x: MyIntSub
# TODO: should pass
static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty))
static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty))
```
An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal
@@ -1144,8 +1016,9 @@ class XAsymmetricProperty:
@x.setter
def x(self, x: int) -> None: ...
static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty))
static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error]
```
A custom descriptor attribute on the nominal class will also suffice:
@@ -1160,8 +1033,9 @@ class Descriptor:
class XCustomDescriptor:
x: Descriptor = Descriptor()
static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty))
static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty))
# TODO: these should pass
static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error]
```
Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a
@@ -1174,20 +1048,19 @@ class HasGetAttr:
def __getattr__(self, attr: str) -> int:
return 42
static_assert(is_subtype_of(HasGetAttr, HasXProperty))
static_assert(is_assignable_to(HasGetAttr, HasXProperty))
# TODO: these should pass
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error]
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error]
static_assert(is_subtype_of(HasGetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))
class HasGetAttrWithUnsuitableReturn:
def __getattr__(self, attr: str) -> tuple[int, int]:
return (1, 2)
# TODO: these should pass
static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error]
static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty))
static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty))
class HasGetAttrAndSetAttr:
def __getattr__(self, attr: str) -> MyInt:
@@ -1195,35 +1068,32 @@ class HasGetAttrAndSetAttr:
def __setattr__(self, attr: str, value: int) -> None: ...
static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty))
static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error]
```
## Narrowing of protocols
<!-- snapshot-diagnostics -->
By default, a protocol class cannot be used as the second argument to `isinstance()` or
`issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the
type inside these branches (this matches the behaviour of other type checkers):
```py
from typing_extensions import Protocol, reveal_type
from typing import Protocol
class HasX(Protocol):
x: int
def f(arg: object, arg2: type):
if isinstance(arg, HasX): # error: [invalid-argument-type]
if isinstance(arg, HasX): # error
reveal_type(arg) # revealed: HasX
else:
reveal_type(arg) # revealed: ~HasX
if issubclass(arg2, HasX): # error: [invalid-argument-type]
if issubclass(arg2, HasX): # error
reveal_type(arg2) # revealed: type[HasX]
else:
reveal_type(arg2) # revealed: type & ~type[HasX]
@@ -1258,10 +1128,10 @@ class OnlyMethodMembers(Protocol):
def method(self) -> None: ...
def f(arg1: type, arg2: type):
if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members)
reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
if issubclass(arg1, OnlyMethodMembers): # error
reveal_type(arg1) # revealed: type[OnlyMethodMembers]
else:
reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
reveal_type(arg1) # revealed: type & ~type[OnlyMethodMembers]
if issubclass(arg2, OnlyMethodMembers): # no error!
reveal_type(arg2) # revealed: type[OnlyMethodMembers]
@@ -1269,143 +1139,6 @@ def f(arg1: type, arg2: type):
reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
```
## Truthiness of protocol instance
An instance of a protocol type generally has ambiguous truthiness:
```py
from typing import Protocol
class Foo(Protocol):
x: int
def f(foo: Foo):
reveal_type(bool(foo)) # revealed: bool
```
But this is not the case if the protocol has a `__bool__` method member that returns `Literal[True]`
or `Literal[False]`:
```py
from typing import Literal
class Truthy(Protocol):
def __bool__(self) -> Literal[True]: ...
class FalsyFoo(Foo, Protocol):
def __bool__(self) -> Literal[False]: ...
class FalsyFooSubclass(FalsyFoo, Protocol):
y: str
def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass):
# TODO should be `Literal[True]
reveal_type(bool(a)) # revealed: bool
# TODO should be `Literal[False]
reveal_type(bool(b)) # revealed: bool
# TODO should be `Literal[False]
reveal_type(bool(c)) # revealed: bool
```
It is not sufficient for a protocol to have a callable `__bool__` instance member that returns
`Literal[True]` for it to be considered always truthy. Dunder methods are looked up on the class
rather than the instance. If a protocol `X` has an instance-attribute `__bool__` member, it is
unknowable whether that attribute can be accessed on the type of an object that satisfies `X`'s
interface:
```py
from typing import Callable
class InstanceAttrBool(Protocol):
__bool__: Callable[[], Literal[True]]
def h(obj: InstanceAttrBool):
reveal_type(bool(obj)) # revealed: bool
```
## Fully static protocols; gradual protocols
A protocol is only fully static if all of its members are fully static:
```py
from typing import Protocol, Any
from knot_extensions import is_fully_static, static_assert
class FullyStatic(Protocol):
x: int
class NotFullyStatic(Protocol):
x: Any
static_assert(is_fully_static(FullyStatic))
# TODO: should pass
static_assert(not is_fully_static(NotFullyStatic)) # error: [static-assert-error]
```
Non-fully-static protocols do not participate in subtyping, only assignability:
```py
from knot_extensions import is_subtype_of, is_assignable_to
class NominalWithX:
x: int = 42
static_assert(is_assignable_to(NominalWithX, FullyStatic))
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
static_assert(is_subtype_of(NominalWithX, FullyStatic))
# TODO: this should pass
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) # error: [static-assert-error]
```
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to
the nominal type `object` (as described above):
```py
class Empty(Protocol): ...
static_assert(is_fully_static(Empty))
```
A method member is only considered fully static if all its parameter annotations and its return
annotation are fully static:
```py
class FullyStaticMethodMember(Protocol):
def method(self, x: int) -> str: ...
class DynamicParameter(Protocol):
def method(self, x: Any) -> str: ...
class DynamicReturn(Protocol):
def method(self, x: int) -> Any: ...
static_assert(is_fully_static(FullyStaticMethodMember))
# TODO: these should pass
static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error]
static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error]
```
The [typing spec][spec_protocol_members] states:
> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any`
Thus, a partially unannotated method member can also not be considered to be fully static:
```py
class NoParameterAnnotation(Protocol):
def method(self, x) -> str: ...
class NoReturnAnnotation(Protocol):
def method(self, x: int): ...
# TODO: these should pass
static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error]
static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error]
```
## `typing.SupportsIndex` and `typing.Sized`
`typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot.
@@ -1434,10 +1167,10 @@ def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
Add tests for:
- Assignments without declarations in protocol class bodies. And various weird ways of creating
attributes in a class body or instance method. [Example mypy tests][mypy_weird_protocols].
- More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec].
- Protocols with instance-method members, including:
- Protocols with methods that have parameters or the return type unannotated
- Protocols with methods that have parameters or the return type annotated with `Any`
- Protocols with instance-method members
- Protocols with `@classmethod` and `@staticmethod`
- Assignability of non-instance types to protocols with instance-method members (e.g. a
class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method)
@@ -1451,13 +1184,16 @@ Add tests for:
- Protocols with instance attributes annotated with `Callable` (can a nominal type with a method
satisfy that protocol, and if so in what cases?)
- Protocols decorated with `@final`
- Protocols with attribute members annotated with `Any`
- Protocols with methods that have parameters or the return type unannotated
- Protocols with methods that have parameters or the return type annotated with `Any`
- Equivalence and subtyping between `Callable` types and protocols that define `__call__`
[mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping
[mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test
[mypy_weird_protocols]: https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d8b/test-data/unit/check-protocols.test#L2131-L2132
[protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests
[protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols
[recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols
[self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols
[spec_protocol_members]: https://typing.python.org/en/latest/spec/protocol.html#protocol-members
[typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html

View File

@@ -13,7 +13,7 @@ if returns_bool():
chr: int = 1
def f():
reveal_type(chr) # revealed: int | (def chr(i: SupportsIndex, /) -> str)
reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str)
```
## Conditionally global or builtin, with annotation
@@ -28,5 +28,5 @@ if returns_bool():
chr: int = 1
def f():
reveal_type(chr) # revealed: int | (def chr(i: SupportsIndex, /) -> str)
reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str)
```

View File

@@ -404,7 +404,7 @@ x = int
class C:
var: ClassVar[x]
reveal_type(C.var) # revealed: str
reveal_type(C.var) # revealed: Unknown | str
x = str
```

View File

@@ -1,177 +0,0 @@
# `global` references
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Explicit global in function
```py
x = 1
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Unassignable type in function
```py
x: int = 1
def f():
y: int = 1
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
y = ""
global x
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
x = ""
```
## Nested intervening scope
A `global` statement causes lookup to skip any bindings in intervening scopes:
```py
x: int = 1
def outer():
x: str = ""
def inner():
global x
# TODO: revealed: int
reveal_type(x) # revealed: str
```
## Narrowing
An assignment following a `global` statement should narrow the type in the local scope after the
assignment.
```py
x: int | None
def f():
global x
x = 1
reveal_type(x) # revealed: Literal[1]
```
## `nonlocal` and `global`
A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython
marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line.
```py
x = 1
def f():
x = 1
def g() -> None:
nonlocal x
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
x = None
```
## Global declaration after `global` statement
```py
def f():
global x
# TODO this should also not be an error
y = x # error: [unresolved-reference] "Name `x` used when not defined"
x = 1 # No error.
x = 2
```
## Semantic syntax errors
Using a name prior to its `global` declaration in the same scope is a syntax error.
```py
x = 1
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
global x
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
global x, y
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
global x
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
global x
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
# still an error in module scope
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
```

View File

@@ -43,3 +43,14 @@ def f():
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```

View File

@@ -5,7 +5,7 @@
```py
class C: ...
C = 1 # error: "Implicit shadowing of class `C`"
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
```
## Explicit

View File

@@ -15,7 +15,7 @@ def f(x: str):
```py
def f(): ...
f = 1 # error: "Implicit shadowing of function `f`"
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
```
## Explicit shadowing

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:11:1
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
11 | instance.attr = 1 # error: [invalid-assignment]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
|
```

View File

@@ -29,12 +29,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:12:1
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
12 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:6:1
|
4 | instance = C()
5 | instance.attr = 1 # fine
6 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
7 |
8 | C.attr = 1 # fine
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna
```
```
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:9:1
|
8 | C.attr = 1 # fine
9 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound
warning: lint:possibly-unbound-attribute
--> /src/mdtest_snippet.py:6:5
|
4 | attr: int = 0
5 |
6 | C.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^
| ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound
7 |
8 | instance = C()
|
@@ -40,12 +40,12 @@ warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]`
```
```
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `C` is possibly unbound
warning: lint:possibly-unbound-attribute
--> /src/mdtest_snippet.py:9:5
|
8 | instance = C()
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:7:1
|
5 | instance = C()
6 | instance.attr = 1 # fine
7 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
|
@@ -40,13 +40,13 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna
```
```
error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]`
error: lint:invalid-attribute-access
--> /src/mdtest_snippet.py:9:1
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
|
```

View File

@@ -37,12 +37,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:11:5
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
11 | C1.attr = 1 # error: [invalid-assignment]
| ^^^^^^^
| ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
12 |
13 | class C2:
|

View File

@@ -23,13 +23,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`.
error: lint:unresolved-attribute
--> /src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`.
4 |
5 | instance = C()
|
@@ -37,12 +37,12 @@ error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `L
```
```
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `C`.
error: lint:unresolved-attribute
--> /src/mdtest_snippet.py:6:1
|
5 | instance = C()
6 | instance.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`.
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:7:1
|
6 | C.attr = 1 # fine
7 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
8 |
9 | instance = C()
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assigna
```
```
error: lint:invalid-attribute-access: Cannot assign to ClassVar `attr` from an instance of type `C`
error: lint:invalid-attribute-access
--> /src/mdtest_snippet.py:10:1
|
9 | instance = C()
10 | instance.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C`
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq`
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import: Cannot resolve import `a.foo`
error: lint:unresolved-import
--> /src/mdtest_snippet.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
| ^^^^^
| ^^^^^ Cannot resolve import `a.foo`
3 |
4 | # Topmost component unresolvable:
|
@@ -40,12 +40,12 @@ error: lint:unresolved-import: Cannot resolve import `a.foo`
```
```
error: lint:unresolved-import: Cannot resolve import `b.foo`
error: lint:unresolved-import
--> /src/mdtest_snippet.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
| ^^^^^
| ^^^^^ Cannot resolve import `b.foo`
|
```

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:10:10
|
9 | # error: [not-iterable]
10 | for x in Iterable():
| ^^^^^^^^^^
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
11 | reveal_type(x) # revealed: int
|

View File

@@ -20,12 +20,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
error: lint:not-iterable
--> /src/mdtest_snippet.py:2:10
|
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
| ^^^^^^^^
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
3 | pass
|

View File

@@ -24,13 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:6:10
|
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
7 | pass
|

View File

@@ -25,12 +25,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:7:10
|
6 | # error: [not-iterable]
7 | for x in Bad():
| ^^^^^
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
8 | reveal_type(x) # revealed: Unknown
|

View File

@@ -46,12 +46,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:22:14
|
21 | # error: [not-iterable]
22 | for x in Iterable1():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
|
@@ -73,12 +73,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:27:14
|
26 | # error: [not-iterable]
27 | for y in Iterable2():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|

View File

@@ -43,12 +43,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:20:14
|
19 | # error: [not-iterable]
20 | for x in Iterable1():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
@@ -70,12 +70,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:25:14
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
26 | reveal_type(y) # revealed: str | int
|

View File

@@ -47,12 +47,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
18 | reveal_type(x) # revealed: int
|
@@ -73,12 +73,12 @@ info: revealed-type: Revealed type
```
```
error: lint: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
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable2():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|

View File

@@ -51,12 +51,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
29 | reveal_type(x) # revealed: int | str
|
@@ -77,12 +77,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:32:14
|
31 | # error: [not-iterable]
32 | for y in Iterable2():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
|

View File

@@ -36,12 +36,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
|
17 | # error: [not-iterable]
18 | for x in Iterable():
| ^^^^^^^^^^
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
19 | reveal_type(x) # revealed: int | bytes
|

View File

@@ -54,12 +54,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:31:14
|
30 | # error: [not-iterable]
31 | for x in Iterable1():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
@@ -81,12 +81,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:36:14
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
37 | reveal_type(y) # revealed: bytes | str | int
|

View File

@@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable():
| ^^^^^^^^^^
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
18 | reveal_type(x) # revealed: int | bytes
|

View File

@@ -36,13 +36,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
|
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
19 | reveal_type(x) # revealed: int
|

View File

@@ -31,13 +31,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
error: lint:not-iterable
--> /src/mdtest_snippet.py:13:14
|
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
| ^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
14 | reveal_type(x) # revealed: int
|

View File

@@ -33,25 +33,25 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:11:14
|
10 | # error: [not-iterable]
11 | for x in NotIterable():
| ^^^^^^^^^^^^^
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
12 | pass
|
```
```
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
warning: lint:possibly-unresolved-reference
--> /src/mdtest_snippet.py:16:17
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| ^
| ^ Name `x` used when possibly not defined
|
```

View File

@@ -26,12 +26,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
error: lint:not-iterable
--> /src/mdtest_snippet.py:8:10
|
7 | # error: [not-iterable]
8 | for x in Bad():
| ^^^^^
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
9 | reveal_type(x) # revealed: Unknown
|

View File

@@ -30,12 +30,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:12:10
|
11 | # error: [not-iterable]
12 | for x in Iterable():
| ^^^^^^^^^^
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
13 | reveal_type(x) # revealed: int
|

View File

@@ -41,12 +41,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable: Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
error: lint:not-iterable
--> /src/mdtest_snippet.py:19:10
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
20 | reveal_type(x) # revealed: int
|
@@ -67,12 +67,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable: Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
error: lint:not-iterable
--> /src/mdtest_snippet.py:23:10
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
| ^^^^^^^^^^^
| ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
24 | reveal_type(y) # revealed: Unknown
|

View File

@@ -24,13 +24,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m
# Diagnostics
```
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:7:8
|
6 | # error: [unsupported-bool-conversion]
7 | 10 and a and True
| ^
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -28,28 +28,26 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
# Diagnostics
```
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | 10 in WithContains()
| ^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
|
info: `__bool__` on `NotBoolable` must be callable
```
```
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:11:1
|
9 | 10 in WithContains()
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
| ^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_mat
# Diagnostics
```
error: lint:no-matching-overload: No overload of class `type` matches arguments
error: lint:no-matching-overload
--> /src/mdtest_snippet.py:1:1
|
1 | type("Foo", ()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments
|
```

View File

@@ -22,13 +22,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
# Diagnostics
```
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
error: lint:unsupported-bool-conversion
--> /src/mdtest_snippet.py:5:1
|
4 | # error: [unsupported-bool-conversion]
5 | not NotBoolable()
| ^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -1,117 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: protocols.md - Protocols - Calls to protocol classes
mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, reveal_type
2 |
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
5 |
6 | class MyProtocol(Protocol):
7 | x: int
8 |
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
```
# Diagnostics
```
error: lint:call-non-callable: Object of type `typing.Protocol` is not callable
--> /src/mdtest_snippet.py:4:13
|
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
| ^^^^^^^^^^
5 |
6 | class MyProtocol(Protocol):
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:4:1
|
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
5 |
6 | class MyProtocol(Protocol):
|
```
```
error: lint:call-non-callable: Cannot instantiate class `MyProtocol`
--> /src/mdtest_snippet.py:10:13
|
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^ This call will raise `TypeError` at runtime
11 | class SubclassOfMyProtocol(MyProtocol): ...
|
info: Protocol classes cannot be instantiated
--> /src/mdtest_snippet.py:6:7
|
4 | reveal_type(Protocol()) # revealed: Unknown
5 |
6 | class MyProtocol(Protocol):
| ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here
7 | x: int
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:10:1
|
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
11 | class SubclassOfMyProtocol(MyProtocol): ...
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:13:1
|
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol`
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:15:5
|
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^ `MyProtocol`
|
```

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