Compare commits
72 Commits
0.12.9
...
ag/salsa-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8acee9db | ||
|
|
ddd4bab67c | ||
|
|
468eb37d75 | ||
|
|
2e9c241d7e | ||
|
|
05478d5cc7 | ||
|
|
4db20f459c | ||
|
|
ec7c2efef9 | ||
|
|
79b2754215 | ||
|
|
a0ddf1f7c4 | ||
|
|
5b00ec981b | ||
|
|
306ef3bb02 | ||
|
|
a4cd13c6e2 | ||
|
|
e0c98874e2 | ||
|
|
f4be05a83b | ||
|
|
1d2128f918 | ||
|
|
276405b44e | ||
|
|
f019cfd15f | ||
|
|
33030b34cd | ||
|
|
656fc335f2 | ||
|
|
e0f4cec7a1 | ||
|
|
662d18bd05 | ||
|
|
c82e255ca8 | ||
|
|
58efd19f11 | ||
|
|
c6dcfe36d0 | ||
|
|
59b078b1bf | ||
|
|
5e943d3539 | ||
|
|
0967e7e088 | ||
|
|
600245478c | ||
|
|
e5c091b850 | ||
|
|
10301f6190 | ||
|
|
4242905b36 | ||
|
|
c20d906503 | ||
|
|
a04375173c | ||
|
|
e6dcdd29f2 | ||
|
|
24f6d2dc13 | ||
|
|
3314cf90ed | ||
|
|
0cb1abc1fc | ||
|
|
f6491cacd1 | ||
|
|
e4f1b587cc | ||
|
|
fbf24be8ae | ||
|
|
5e4fa9e442 | ||
|
|
67529edad6 | ||
|
|
4ac2b2c222 | ||
|
|
083bb85d9d | ||
|
|
c7af595fc1 | ||
|
|
7d8f7c20da | ||
|
|
76c933d10e | ||
|
|
d423191d94 | ||
|
|
c8d155b2b9 | ||
|
|
a5339a52c3 | ||
|
|
48772c04d7 | ||
|
|
510a07dee2 | ||
|
|
47d44e5f7b | ||
|
|
ec3163781c | ||
|
|
b892e4548e | ||
|
|
9ac39cee98 | ||
|
|
f4d8826428 | ||
|
|
527a690a73 | ||
|
|
f0e9c1d8f9 | ||
|
|
2e1d6623cd | ||
|
|
2dc2f68b0f | ||
|
|
26d6c3831f | ||
|
|
9ced219ffc | ||
|
|
f344dda82c | ||
|
|
6de84ed56e | ||
|
|
bd4506aac5 | ||
|
|
0e5577ab56 | ||
|
|
957320c0f1 | ||
|
|
f6093452ed | ||
|
|
82350a398e | ||
|
|
ce938fe205 | ||
|
|
7f8f1ab2c1 |
16
.github/workflows/build-binaries.yml
vendored
16
.github/workflows/build-binaries.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
@@ -372,7 +372,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -437,7 +437,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -715,7 +715,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
|
||||
1
.github/workflows/mypy_primer.yaml
vendored
1
.github/workflows/mypy_primer.yaml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
- "crates/ruff_python_parser"
|
||||
- ".github/workflows/mypy_primer.yaml"
|
||||
- ".github/workflows/mypy_primer_comment.yaml"
|
||||
- "scripts/mypy_primer.sh"
|
||||
- "Cargo.lock"
|
||||
- "!**.md"
|
||||
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
11
.github/workflows/typing_conformance.yaml
vendored
11
.github/workflows/typing_conformance.yaml
vendored
@@ -54,6 +54,9 @@ jobs:
|
||||
|
||||
- name: Compute diagnostic diff
|
||||
shell: bash
|
||||
env:
|
||||
# TODO: Remove this once we fixed the remaining panics in the conformance suite.
|
||||
TY_MAX_PARALLELISM: 1
|
||||
run: |
|
||||
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
|
||||
|
||||
@@ -63,15 +66,15 @@ jobs:
|
||||
|
||||
echo "new commit"
|
||||
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-new
|
||||
cargo build --bin ty
|
||||
mv target/debug/ty ty-new
|
||||
|
||||
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
|
||||
git checkout -b old_commit "$MERGE_BASE"
|
||||
echo "old commit (merge base)"
|
||||
git rev-list --format=%s --max-count=1 old_commit
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-old
|
||||
cargo build --bin ty
|
||||
mv target/debug/ty ty-old
|
||||
)
|
||||
|
||||
(
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -24,8 +24,31 @@
|
||||
### Other changes
|
||||
|
||||
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
|
||||
|
||||
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
|
||||
|
||||
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
|
||||
|
||||
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
|
||||
|
||||
```diff
|
||||
-unused.py:8:19: F401 [*] `pathlib` imported but unused
|
||||
+F401 [*] `pathlib` imported but unused
|
||||
+ --> unused.py:8:19
|
||||
|
|
||||
7 | # Unused, _not_ marked as required (due to the alias).
|
||||
8 | import pathlib as non_alias
|
||||
- | ^^^^^^^^^ F401
|
||||
+ | ^^^^^^^^^
|
||||
9 |
|
||||
10 | # Unused, marked as required.
|
||||
|
|
||||
- = help: Remove unused import: `pathlib`
|
||||
+help: Remove unused import: `pathlib`
|
||||
```
|
||||
|
||||
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
|
||||
|
||||
## 0.12.8
|
||||
|
||||
### Preview features
|
||||
|
||||
74
Cargo.lock
generated
74
Cargo.lock
generated
@@ -128,9 +128,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -257,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
@@ -408,9 +408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.43"
|
||||
version = "4.5.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
|
||||
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -418,9 +418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.43"
|
||||
version = "4.5.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
|
||||
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -461,9 +461,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.41"
|
||||
version = "4.5.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -1218,9 +1218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
@@ -1241,7 +1241,7 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -1521,7 +1521,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
@@ -1764,9 +1764,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1809,7 +1809,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
@@ -2014,7 +2014,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2026,7 +2026,7 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2054,7 +2054,7 @@ version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
@@ -2666,7 +2666,7 @@ version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2749,7 +2749,7 @@ dependencies = [
|
||||
"argfile",
|
||||
"assert_fs",
|
||||
"bincode 2.0.1",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"cachedir",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
@@ -3000,7 +3000,7 @@ version = "0.12.9"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"clap",
|
||||
"colored 3.0.0",
|
||||
"fern",
|
||||
@@ -3106,7 +3106,7 @@ name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"is-macro",
|
||||
@@ -3194,7 +3194,7 @@ dependencies = [
|
||||
name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"itertools 0.14.0",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
@@ -3205,7 +3205,7 @@ name = "ruff_python_parser"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"bstr",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
@@ -3230,7 +3230,7 @@ dependencies = [
|
||||
name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
@@ -3251,7 +3251,7 @@ dependencies = [
|
||||
name = "ruff_python_stdlib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -3428,7 +3428,7 @@ version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -3450,7 +3450,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=918d35d873b2b73a0237536144ef4d22e8d57f27#918d35d873b2b73a0237536144ef4d22e8d57f27"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3474,12 +3474,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=918d35d873b2b73a0237536144ef4d22e8d57f27#918d35d873b2b73a0237536144ef4d22e8d57f27"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=918d35d873b2b73a0237536144ef4d22e8d57f27#918d35d873b2b73a0237536144ef4d22e8d57f27"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4238,7 +4238,7 @@ dependencies = [
|
||||
name = "ty_ide"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"regex",
|
||||
@@ -4297,7 +4297,7 @@ name = "ty_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"bitvec",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
@@ -4350,7 +4350,7 @@ name = "ty_server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"insta",
|
||||
@@ -4393,7 +4393,7 @@ name = "ty_test"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"insta",
|
||||
@@ -5143,7 +5143,7 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -5,7 +5,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
# Please update rustfmt.toml when bumping the Rust edition
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
rust-version = "1.87"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -143,7 +143,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "918d35d873b2b73a0237536144ef4d22e8d57f27", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -5588,15 +5588,15 @@ fn cookiecutter_globbing() -> Result<()> {
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--select=F811")
|
||||
.current_dir(tempdir.path()), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -5801,3 +5801,32 @@ fn future_annotations_preview_warning() {
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up045_nested_optional_flatten_all() {
|
||||
let contents = "\
|
||||
from typing import Optional
|
||||
nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
";
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--select", "UP045", "--diff", "--target-version", "py312"])
|
||||
.arg("-")
|
||||
.pass_stdin(contents),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -1,2 +1,2 @@
|
||||
from typing import Optional
|
||||
-nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
+nested_optional: str | None = None
|
||||
|
||||
|
||||
----- stderr -----
|
||||
Would fix 1 error.
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,6 +254,11 @@ impl Diagnostic {
|
||||
.find(|ann| ann.is_primary)
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of all annotations of this diagnostic.
|
||||
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
|
||||
Arc::make_mut(&mut self.inner).annotations.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the "primary" span of this diagnostic if one exists.
|
||||
///
|
||||
/// When there are multiple primary spans, then the first one that was
|
||||
@@ -310,6 +315,11 @@ impl Diagnostic {
|
||||
&self.inner.subs
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of the sub-diagnostics of this diagnostic.
|
||||
pub fn sub_diagnostics_mut(&mut self) -> impl Iterator<Item = &mut SubDiagnostic> {
|
||||
Arc::make_mut(&mut self.inner).subs.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the fix for this diagnostic if it exists.
|
||||
pub fn fix(&self) -> Option<&Fix> {
|
||||
self.inner.fix.as_ref()
|
||||
@@ -621,6 +631,11 @@ impl SubDiagnostic {
|
||||
&self.inner.annotations
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of the annotations of this sub-diagnostic.
|
||||
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
|
||||
self.inner.annotations.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns a shared borrow of the "primary" annotation of this diagnostic
|
||||
/// if one exists.
|
||||
///
|
||||
|
||||
@@ -264,7 +264,12 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = ann.span.file.path(resolver);
|
||||
let path = ann
|
||||
.span
|
||||
.file
|
||||
.relative_path(resolver)
|
||||
.to_str()
|
||||
.unwrap_or_else(|| ann.span.file.path(resolver));
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
|
||||
})
|
||||
|
||||
@@ -87,11 +87,12 @@ impl Files {
|
||||
.system_by_path
|
||||
.entry(absolute.clone())
|
||||
.or_insert_with(|| {
|
||||
tracing::trace!("Adding file '{path}'");
|
||||
|
||||
let metadata = db.system().path_metadata(path);
|
||||
|
||||
tracing::trace!("Adding file '{absolute}'");
|
||||
|
||||
let durability = self
|
||||
.root(db, path)
|
||||
.root(db, &absolute)
|
||||
.map_or(Durability::default(), |root| root.durability(db));
|
||||
|
||||
let builder = File::builder(FilePath::System(absolute))
|
||||
|
||||
@@ -166,3 +166,7 @@ r"""first
|
||||
print("S\x1cP\x1dL\x1eI\x1fT".split())
|
||||
print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
|
||||
print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
|
||||
|
||||
# leading/trailing whitespace should not count towards maxsplit
|
||||
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
|
||||
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Hello, world!"""\
|
||||
\
|
||||
|
||||
x = 1; y = 2
|
||||
@@ -69,3 +69,10 @@ a7: OptionalTE[typing.NamedTuple] = None
|
||||
a8: typing_extensions.Optional[typing.NamedTuple] = None
|
||||
a9: "Optional[NamedTuple]" = None
|
||||
a10: Optional[NamedTupleTE] = None
|
||||
|
||||
|
||||
# Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
# Nested Optional types should be flattened
|
||||
nested_optional: Optional[Optional[str]] = None
|
||||
nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
||||
@@ -28,7 +28,7 @@ use itertools::Itertools;
|
||||
use log::debug;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, IntoDiagnosticMessage, Span};
|
||||
use ruff_diagnostics::{Applicability, Fix, IsolationLevel};
|
||||
use ruff_notebook::{CellOffsets, NotebookIndex};
|
||||
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
|
||||
@@ -3305,6 +3305,17 @@ impl DiagnosticGuard<'_, '_> {
|
||||
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a secondary annotation with the given message and range.
|
||||
pub(crate) fn secondary_annotation<'a>(
|
||||
&mut self,
|
||||
message: impl IntoDiagnosticMessage + 'a,
|
||||
range: impl Ranged,
|
||||
) {
|
||||
let span = Span::from(self.context.source_file.clone()).with_range(range.range());
|
||||
let ann = Annotation::secondary(span).message(message);
|
||||
self.diagnostic.as_mut().unwrap().annotate(ann);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DiagnosticGuard<'_, '_> {
|
||||
|
||||
@@ -63,9 +63,9 @@ impl<'a> Insertion<'a> {
|
||||
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
|
||||
}
|
||||
|
||||
// If the first token after the docstring is a continuation character (i.e. "\"), advance
|
||||
// an additional row to prevent inserting in the same logical line.
|
||||
if match_continuation(locator.after(location)).is_some() {
|
||||
// While the first token after the docstring is a continuation character (i.e. "\"), advance
|
||||
// additional rows to prevent inserting in the same logical line.
|
||||
while match_continuation(locator.after(location)).is_some() {
|
||||
location = locator.full_line_end(location);
|
||||
}
|
||||
|
||||
@@ -379,6 +379,17 @@ mod tests {
|
||||
Insertion::own_line("", TextSize::from(22), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
"""Hello, world!"""\
|
||||
\
|
||||
|
||||
"#
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::own_line("", TextSize::from(24), "\n")
|
||||
);
|
||||
|
||||
let contents = r"
|
||||
x = 1
|
||||
"
|
||||
|
||||
@@ -230,3 +230,8 @@ pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterS
|
||||
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19851
|
||||
pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -485,9 +485,6 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// from typing import Any
|
||||
///
|
||||
///
|
||||
/// def foo(x: int): ...
|
||||
/// ```
|
||||
///
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{checkers::ast::Checker, fix};
|
||||
/// statement has no effect and should be omitted.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Static Typing with Python: Type Stubs](https://typing.python.org/en/latest/source/stubs.html)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#language-features)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct FutureAnnotationsInStub;
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
|
||||
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -9,6 +9,8 @@ use ruff_python_ast::{
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_maxsplit_without_separator_fix_enabled;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -84,7 +86,9 @@ pub(crate) fn split_static_string(
|
||||
let sep_arg = arguments.find_argument_value("sep", 0);
|
||||
let split_replacement = if let Some(sep) = sep_arg {
|
||||
match sep {
|
||||
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value, direction),
|
||||
Expr::NoneLiteral(_) => {
|
||||
split_default(str_value, maxsplit_value, direction, checker.settings())
|
||||
}
|
||||
Expr::StringLiteral(sep_value) => {
|
||||
let sep_value_str = sep_value.value.to_str();
|
||||
Some(split_sep(
|
||||
@@ -100,7 +104,7 @@ pub(crate) fn split_static_string(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
split_default(str_value, maxsplit_value, direction)
|
||||
split_default(str_value, maxsplit_value, direction, checker.settings())
|
||||
};
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
|
||||
@@ -174,6 +178,7 @@ fn split_default(
|
||||
str_value: &StringLiteralValue,
|
||||
max_split: i32,
|
||||
direction: Direction,
|
||||
settings: &LinterSettings,
|
||||
) -> Option<Expr> {
|
||||
// From the Python documentation:
|
||||
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
|
||||
@@ -185,10 +190,31 @@ fn split_default(
|
||||
let string_val = str_value.to_str();
|
||||
match max_split.cmp(&0) {
|
||||
Ordering::Greater => {
|
||||
// Autofix for `maxsplit` without separator not yet implemented, as
|
||||
// `split_whitespace().remainder()` is not stable:
|
||||
// https://doc.rust-lang.org/std/str/struct.SplitWhitespace.html#method.remainder
|
||||
None
|
||||
if !is_maxsplit_without_separator_fix_enabled(settings) {
|
||||
return None;
|
||||
}
|
||||
let Ok(max_split) = usize::try_from(max_split) else {
|
||||
return None;
|
||||
};
|
||||
let list_items: Vec<&str> = if direction == Direction::Left {
|
||||
string_val
|
||||
.trim_start_matches(py_unicode_is_whitespace)
|
||||
.splitn(max_split + 1, py_unicode_is_whitespace)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
} else {
|
||||
let mut items: Vec<&str> = string_val
|
||||
.trim_end_matches(py_unicode_is_whitespace)
|
||||
.rsplitn(max_split + 1, py_unicode_is_whitespace)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
items.reverse();
|
||||
items
|
||||
};
|
||||
Some(construct_replacement(
|
||||
&list_items,
|
||||
str_value.first_literal_flags(),
|
||||
))
|
||||
}
|
||||
Ordering::Equal => {
|
||||
// Behavior for maxsplit = 0 when sep is None:
|
||||
|
||||
@@ -1439,6 +1439,7 @@ help: Replace with list literal
|
||||
166 |+print(["S", "P", "L", "I", "T"])
|
||||
167 167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
|
||||
168 168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
|
||||
169 169 |
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:167:7
|
||||
@@ -1458,6 +1459,8 @@ help: Replace with list literal
|
||||
167 |-print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
|
||||
167 |+print([">"])
|
||||
168 168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
|
||||
169 169 |
|
||||
170 170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:168:7
|
||||
@@ -1466,6 +1469,8 @@ SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
|
||||
168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
169 |
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
|
|
||||
help: Replace with list literal
|
||||
|
||||
@@ -1475,3 +1480,26 @@ help: Replace with list literal
|
||||
167 167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
|
||||
168 |-print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
|
||||
168 |+print(["<"])
|
||||
169 169 |
|
||||
170 170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
171 171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
|
||||
|
||||
SIM905 Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:171:1
|
||||
|
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
||||
|
|
||||
help: Replace with list literal
|
||||
|
||||
SIM905 Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:172:1
|
||||
|
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
|
||||
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Replace with list literal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -797,6 +797,7 @@ mod tests {
|
||||
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
|
||||
#[test_case(Path::new("docstring_only.py"))]
|
||||
#[test_case(Path::new("docstring_with_continuation.py"))]
|
||||
#[test_case(Path::new("docstring_with_multiple_continuations.py"))]
|
||||
#[test_case(Path::new("docstring_with_semicolon.py"))]
|
||||
#[test_case(Path::new("empty.py"))]
|
||||
#[test_case(Path::new("existing_import.py"))]
|
||||
@@ -832,6 +833,7 @@ mod tests {
|
||||
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
|
||||
#[test_case(Path::new("docstring_only.py"))]
|
||||
#[test_case(Path::new("docstring_with_continuation.py"))]
|
||||
#[test_case(Path::new("docstring_with_multiple_continuations.py"))]
|
||||
#[test_case(Path::new("docstring_with_semicolon.py"))]
|
||||
#[test_case(Path::new("empty.py"))]
|
||||
#[test_case(Path::new("existing_import.py"))]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
I002 [*] Missing required import: `from __future__ import annotations`
|
||||
--> docstring_with_multiple_continuations.py:1:1
|
||||
help: Insert required import: `from __future__ import annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | """Hello, world!"""\
|
||||
2 2 | \
|
||||
3 3 |
|
||||
4 |+from __future__ import annotations
|
||||
4 5 | x = 1; y = 2
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
I002 [*] Missing required import: `from __future__ import annotations as _annotations`
|
||||
--> docstring_with_multiple_continuations.py:1:1
|
||||
help: Insert required import: `from __future__ import annotations as _annotations`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | """Hello, world!"""\
|
||||
2 2 | \
|
||||
3 3 |
|
||||
4 |+from __future__ import annotations as _annotations
|
||||
4 5 | x = 1; y = 2
|
||||
@@ -60,7 +60,7 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1;
|
||||
/// ## References
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BlankLineBetweenMethods;
|
||||
|
||||
@@ -113,7 +113,7 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
|
||||
/// ## References
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BlankLinesTopLevel {
|
||||
actual_blank_lines: u32,
|
||||
@@ -180,7 +180,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
|
||||
/// ## References
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct TooManyBlankLines {
|
||||
actual_blank_lines: u32,
|
||||
@@ -277,7 +277,7 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator {
|
||||
/// ## References
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BlankLinesAfterFunctionOrClass {
|
||||
actual_blank_lines: u32,
|
||||
@@ -331,7 +331,7 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
|
||||
/// ## References
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
|
||||
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BlankLinesBeforeNestedDefinition;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_source_file::UniversalNewlines;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for lambda expressions which are assigned to a variable.
|
||||
@@ -105,29 +105,24 @@ pub(crate) fn lambda_assignment(
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, if the assignment is in a class body, flag it, but use a display-only fix.
|
||||
// Rewriting safely would require making this a static method.
|
||||
//
|
||||
// Similarly, if the lambda is shadowing a variable in the current scope,
|
||||
// If the lambda is shadowing a variable in the current scope,
|
||||
// rewriting it as a function declaration may break type-checking.
|
||||
// See: https://github.com/astral-sh/ruff/issues/5421
|
||||
if checker.semantic().current_scope().kind.is_class()
|
||||
|| checker
|
||||
.semantic()
|
||||
.current_scope()
|
||||
.get_all(id)
|
||||
.any(|binding_id| checker.semantic().binding(binding_id).kind.is_annotation())
|
||||
let applicability = if checker
|
||||
.semantic()
|
||||
.current_scope()
|
||||
.get_all(id)
|
||||
.any(|binding_id| checker.semantic().binding(binding_id).kind.is_annotation())
|
||||
{
|
||||
diagnostic.set_fix(Fix::display_only_edit(Edit::range_replacement(
|
||||
indented,
|
||||
stmt.range(),
|
||||
)));
|
||||
Applicability::DisplayOnly
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
indented,
|
||||
stmt.range(),
|
||||
)));
|
||||
}
|
||||
Applicability::Unsafe
|
||||
};
|
||||
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
Edit::range_replacement(indented, stmt.range()),
|
||||
applicability,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ help: Rewrite `f` as a `def`
|
||||
26 27 |
|
||||
27 28 | def scope():
|
||||
|
||||
E731 Do not assign a `lambda` expression, use a `def`
|
||||
E731 [*] Do not assign a `lambda` expression, use a `def`
|
||||
--> E731.py:57:5
|
||||
|
|
||||
55 | class Scope:
|
||||
@@ -115,7 +115,7 @@ E731 Do not assign a `lambda` expression, use a `def`
|
||||
|
|
||||
help: Rewrite `f` as a `def`
|
||||
|
||||
ℹ Display-only fix
|
||||
ℹ Unsafe fix
|
||||
54 54 |
|
||||
55 55 | class Scope:
|
||||
56 56 | # E731
|
||||
@@ -318,7 +318,7 @@ help: Rewrite `f` as a `def`
|
||||
137 138 |
|
||||
138 139 | class TemperatureScales(Enum):
|
||||
|
||||
E731 Do not assign a `lambda` expression, use a `def`
|
||||
E731 [*] Do not assign a `lambda` expression, use a `def`
|
||||
--> E731.py:139:5
|
||||
|
|
||||
138 | class TemperatureScales(Enum):
|
||||
@@ -328,7 +328,7 @@ E731 Do not assign a `lambda` expression, use a `def`
|
||||
|
|
||||
help: Rewrite `CELSIUS` as a `def`
|
||||
|
||||
ℹ Display-only fix
|
||||
ℹ Unsafe fix
|
||||
136 136 |
|
||||
137 137 |
|
||||
138 138 | class TemperatureScales(Enum):
|
||||
@@ -339,7 +339,7 @@ help: Rewrite `CELSIUS` as a `def`
|
||||
141 142 |
|
||||
142 143 |
|
||||
|
||||
E731 Do not assign a `lambda` expression, use a `def`
|
||||
E731 [*] Do not assign a `lambda` expression, use a `def`
|
||||
--> E731.py:140:5
|
||||
|
|
||||
138 | class TemperatureScales(Enum):
|
||||
@@ -349,7 +349,7 @@ E731 Do not assign a `lambda` expression, use a `def`
|
||||
|
|
||||
help: Rewrite `FAHRENHEIT` as a `def`
|
||||
|
||||
ℹ Display-only fix
|
||||
ℹ Unsafe fix
|
||||
137 137 |
|
||||
138 138 | class TemperatureScales(Enum):
|
||||
139 139 | CELSIUS = (lambda deg_c: deg_c)
|
||||
|
||||
@@ -183,14 +183,24 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
|
||||
// Create diagnostics for each statement.
|
||||
for (source, entries) in &redefinitions {
|
||||
for (shadowed, binding) in entries {
|
||||
let name = binding.name(checker.source());
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
RedefinedWhileUnused {
|
||||
name: binding.name(checker.source()).to_string(),
|
||||
name: name.to_string(),
|
||||
row: checker.compute_source_row(shadowed.start()),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
diagnostic.secondary_annotation(
|
||||
format_args!("previous definition of `{name}` here"),
|
||||
shadowed,
|
||||
);
|
||||
|
||||
if let Some(ann) = diagnostic.primary_annotation_mut() {
|
||||
ann.set_message(format_args!("`{name}` redefined here"));
|
||||
}
|
||||
|
||||
if let Some(range) = binding.parent_range(checker.semantic()) {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
|
||||
@@ -5,7 +5,14 @@ F811 Redefinition of unused `bar` from line 6
|
||||
--> F811_0.py:10:5
|
||||
|
|
||||
10 | def bar():
|
||||
| ^^^
|
||||
| ^^^ `bar` redefined here
|
||||
11 | pass
|
||||
|
|
||||
::: F811_0.py:6:5
|
||||
|
|
||||
5 | @foo
|
||||
6 | def bar():
|
||||
| --- previous definition of `bar` here
|
||||
7 | pass
|
||||
|
|
||||
help: Remove definition: `bar`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `FU` from line 1
|
||||
--> F811_1.py:1:25
|
||||
--> F811_1.py:1:14
|
||||
|
|
||||
1 | import fu as FU, bar as FU
|
||||
| ^^
|
||||
| -- ^^ `FU` redefined here
|
||||
| |
|
||||
| previous definition of `FU` here
|
||||
|
|
||||
help: Remove definition: `FU`
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `mixer` from line 2
|
||||
--> F811_12.py:6:20
|
||||
--> F811_12.py:2:20
|
||||
|
|
||||
1 | try:
|
||||
2 | from aa import mixer
|
||||
| ----- previous definition of `mixer` here
|
||||
3 | except ImportError:
|
||||
4 | pass
|
||||
5 | else:
|
||||
6 | from bb import mixer
|
||||
| ^^^^^
|
||||
| ^^^^^ `mixer` redefined here
|
||||
7 | mixer(123)
|
||||
|
|
||||
help: Remove definition: `mixer`
|
||||
|
||||
@@ -5,7 +5,12 @@ F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_15.py:4:5
|
||||
|
|
||||
4 | def fu():
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
5 | pass
|
||||
|
|
||||
::: F811_15.py:1:8
|
||||
|
|
||||
1 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -7,7 +7,14 @@ F811 Redefinition of unused `fu` from line 3
|
||||
6 | def bar():
|
||||
7 | def baz():
|
||||
8 | def fu():
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
9 | pass
|
||||
|
|
||||
::: F811_16.py:3:8
|
||||
|
|
||||
1 | """Test that shadowing a global with a nested function generates a warning."""
|
||||
2 |
|
||||
3 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -6,10 +6,16 @@ F811 [*] Redefinition of unused `fu` from line 2
|
||||
|
|
||||
5 | def bar():
|
||||
6 | import fu
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
7 |
|
||||
8 | def baz():
|
||||
|
|
||||
::: F811_17.py:2:8
|
||||
|
|
||||
1 | """Test that shadowing a global name with a nested function generates a warning."""
|
||||
2 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
ℹ Safe fix
|
||||
@@ -22,11 +28,15 @@ help: Remove definition: `fu`
|
||||
9 8 | def fu():
|
||||
|
||||
F811 Redefinition of unused `fu` from line 6
|
||||
--> F811_17.py:9:13
|
||||
--> F811_17.py:6:12
|
||||
|
|
||||
5 | def bar():
|
||||
6 | import fu
|
||||
| -- previous definition of `fu` here
|
||||
7 |
|
||||
8 | def baz():
|
||||
9 | def fu():
|
||||
| ^^
|
||||
| ^^ `fu` redefined here
|
||||
10 | pass
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `FU` from line 1
|
||||
--> F811_2.py:1:34
|
||||
--> F811_2.py:1:23
|
||||
|
|
||||
1 | from moo import fu as FU, bar as FU
|
||||
| ^^
|
||||
| -- ^^ `FU` redefined here
|
||||
| |
|
||||
| previous definition of `FU` here
|
||||
|
|
||||
help: Remove definition: `FU`
|
||||
|
||||
@@ -7,9 +7,17 @@ F811 [*] Redefinition of unused `Sequence` from line 26
|
||||
30 | from typing import (
|
||||
31 | List, # noqa: F811
|
||||
32 | Sequence,
|
||||
| ^^^^^^^^
|
||||
| ^^^^^^^^ `Sequence` redefined here
|
||||
33 | )
|
||||
|
|
||||
::: F811_21.py:26:5
|
||||
|
|
||||
24 | from typing import (
|
||||
25 | List, # noqa
|
||||
26 | Sequence, # noqa
|
||||
| -------- previous definition of `Sequence` here
|
||||
27 | )
|
||||
|
|
||||
help: Remove definition: `Sequence`
|
||||
|
||||
ℹ Safe fix
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `foo` from line 3
|
||||
--> F811_23.py:4:15
|
||||
--> F811_23.py:3:15
|
||||
|
|
||||
1 | """Test that shadowing an explicit re-export produces a warning."""
|
||||
2 |
|
||||
3 | import foo as foo
|
||||
| --- previous definition of `foo` here
|
||||
4 | import bar as foo
|
||||
| ^^^
|
||||
| ^^^ `foo` redefined here
|
||||
|
|
||||
help: Remove definition: `foo`
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `func` from line 2
|
||||
--> F811_26.py:5:9
|
||||
--> F811_26.py:2:9
|
||||
|
|
||||
1 | class Class:
|
||||
2 | def func(self):
|
||||
| ---- previous definition of `func` here
|
||||
3 | pass
|
||||
4 |
|
||||
5 | def func(self):
|
||||
| ^^^^
|
||||
| ^^^^ `func` redefined here
|
||||
6 | pass
|
||||
|
|
||||
help: Remove definition: `func`
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `datetime` from line 3
|
||||
--> F811_28.py:4:22
|
||||
--> F811_28.py:3:8
|
||||
|
|
||||
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10384"""
|
||||
2 |
|
||||
3 | import datetime
|
||||
| -------- previous definition of `datetime` here
|
||||
4 | from datetime import datetime
|
||||
| ^^^^^^^^
|
||||
| ^^^^^^^^ `datetime` redefined here
|
||||
5 |
|
||||
6 | datetime(1, 2, 3)
|
||||
|
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `Bar` from line 3
|
||||
--> F811_29.pyi:8:1
|
||||
--> F811_29.pyi:3:24
|
||||
|
|
||||
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10509"""
|
||||
2 |
|
||||
3 | from foo import Bar as Bar
|
||||
| --- previous definition of `Bar` here
|
||||
4 |
|
||||
5 | class Eggs:
|
||||
6 | Bar: int # OK
|
||||
7 |
|
||||
8 | Bar = 1 # F811
|
||||
| ^^^
|
||||
| ^^^ `Bar` redefined here
|
||||
|
|
||||
help: Remove definition: `Bar`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_3.py:1:12
|
||||
--> F811_3.py:1:8
|
||||
|
|
||||
1 | import fu; fu = 3
|
||||
| ^^
|
||||
| -- ^^ `fu` redefined here
|
||||
| |
|
||||
| previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,32 +2,43 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `bar` from line 10
|
||||
--> F811_30.py:12:9
|
||||
--> F811_30.py:10:5
|
||||
|
|
||||
8 | """Foo."""
|
||||
9 |
|
||||
10 | bar = foo
|
||||
| --- previous definition of `bar` here
|
||||
11 |
|
||||
12 | def bar(self) -> None:
|
||||
| ^^^
|
||||
| ^^^ `bar` redefined here
|
||||
13 | """Bar."""
|
||||
|
|
||||
help: Remove definition: `bar`
|
||||
|
||||
F811 Redefinition of unused `baz` from line 18
|
||||
--> F811_30.py:21:5
|
||||
--> F811_30.py:18:9
|
||||
|
|
||||
16 | class B:
|
||||
17 | """B."""
|
||||
18 | def baz(self) -> None:
|
||||
| --- previous definition of `baz` here
|
||||
19 | """Baz."""
|
||||
20 |
|
||||
21 | baz = 1
|
||||
| ^^^
|
||||
| ^^^ `baz` redefined here
|
||||
|
|
||||
help: Remove definition: `baz`
|
||||
|
||||
F811 Redefinition of unused `foo` from line 26
|
||||
--> F811_30.py:29:12
|
||||
--> F811_30.py:26:9
|
||||
|
|
||||
24 | class C:
|
||||
25 | """C."""
|
||||
26 | def foo(self) -> None:
|
||||
| --- previous definition of `foo` here
|
||||
27 | """Foo."""
|
||||
28 |
|
||||
29 | bar = (foo := 1)
|
||||
| ^^^
|
||||
| ^^^ `foo` redefined here
|
||||
|
|
||||
help: Remove definition: `foo`
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `baz` from line 17
|
||||
--> F811_31.py:19:29
|
||||
--> F811_31.py:17:5
|
||||
|
|
||||
16 | try:
|
||||
17 | baz = None
|
||||
| --- previous definition of `baz` here
|
||||
18 |
|
||||
19 | from some_module import baz
|
||||
| ^^^
|
||||
| ^^^ `baz` redefined here
|
||||
20 | except ImportError:
|
||||
21 | pass
|
||||
|
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `List` from line 4
|
||||
--> F811_32.py:5:5
|
||||
--> F811_32.py:4:5
|
||||
|
|
||||
3 | from typing import (
|
||||
4 | List,
|
||||
| ---- previous definition of `List` here
|
||||
5 | List,
|
||||
| ^^^^
|
||||
| ^^^^ `List` redefined here
|
||||
6 | )
|
||||
|
|
||||
help: Remove definition: `List`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_4.py:1:12
|
||||
--> F811_4.py:1:8
|
||||
|
|
||||
1 | import fu; fu, bar = 3
|
||||
| ^^
|
||||
| -- ^^ `fu` redefined here
|
||||
| |
|
||||
| previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 Redefinition of unused `fu` from line 1
|
||||
--> F811_5.py:1:13
|
||||
--> F811_5.py:1:8
|
||||
|
|
||||
1 | import fu; [fu, bar] = 3
|
||||
| ^^
|
||||
| -- ^^ `fu` redefined here
|
||||
| |
|
||||
| previous definition of `fu` here
|
||||
|
|
||||
help: Remove definition: `fu`
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `os` from line 5
|
||||
--> F811_6.py:6:12
|
||||
--> F811_6.py:5:12
|
||||
|
|
||||
3 | i = 2
|
||||
4 | if i == 1:
|
||||
5 | import os
|
||||
| -- previous definition of `os` here
|
||||
6 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
7 | os.path
|
||||
|
|
||||
help: Remove definition: `os`
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `os` from line 4
|
||||
--> F811_8.py:5:12
|
||||
--> F811_8.py:4:12
|
||||
|
|
||||
3 | try:
|
||||
4 | import os
|
||||
| -- previous definition of `os` here
|
||||
5 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
6 | except:
|
||||
7 | pass
|
||||
|
|
||||
|
||||
@@ -19,11 +19,14 @@ help: Remove unused import: `os`
|
||||
5 4 | import os
|
||||
|
||||
F811 [*] Redefinition of unused `os` from line 2
|
||||
--> <filename>:5:12
|
||||
--> <filename>:2:8
|
||||
|
|
||||
2 | import os
|
||||
| -- previous definition of `os` here
|
||||
3 |
|
||||
4 | def f():
|
||||
5 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
6 |
|
||||
7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused
|
||||
|
|
||||
|
||||
@@ -19,11 +19,14 @@ help: Remove unused import: `os`
|
||||
5 4 | os = 1
|
||||
|
||||
F811 Redefinition of unused `os` from line 2
|
||||
--> <filename>:5:5
|
||||
--> <filename>:2:8
|
||||
|
|
||||
2 | import os
|
||||
| -- previous definition of `os` here
|
||||
3 |
|
||||
4 | def f():
|
||||
5 | os = 1
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
6 | print(os)
|
||||
|
|
||||
help: Remove definition: `os`
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F811 [*] Redefinition of unused `os` from line 3
|
||||
--> <filename>:4:12
|
||||
--> <filename>:3:12
|
||||
|
|
||||
2 | def f():
|
||||
3 | import os
|
||||
| -- previous definition of `os` here
|
||||
4 | import os
|
||||
| ^^
|
||||
| ^^ `os` redefined here
|
||||
5 |
|
||||
6 | # Despite this `del`, `import os` should still be flagged as shadowing an unused
|
||||
|
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_ast::helpers::{pep_604_optional, pep_604_union};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::analyze::typing::Pep604Operator;
|
||||
use ruff_python_semantic::analyze::typing::{Pep604Operator, to_pep604_operator};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -171,10 +171,22 @@ pub(crate) fn non_pep604_annotation(
|
||||
// Invalid type annotation.
|
||||
}
|
||||
_ => {
|
||||
// Unwrap all nested Optional[...] and wrap once as `X | None`.
|
||||
let mut inner = slice;
|
||||
while let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = inner {
|
||||
if let Some(Pep604Operator::Optional) =
|
||||
to_pep604_operator(value, slice, checker.semantic())
|
||||
{
|
||||
inner = slice;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
Edit::range_replacement(
|
||||
pad(
|
||||
checker.generator().expr(&pep_604_optional(slice)),
|
||||
checker.generator().expr(&pep_604_optional(inner)),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
),
|
||||
|
||||
@@ -171,3 +171,134 @@ UP045 Use `X | None` for type annotations
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:76:18
|
||||
|
|
||||
74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
75 | # Nested Optional types should be flattened
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
73 73 |
|
||||
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 |-nested_optional: Optional[Optional[str]] = None
|
||||
76 |+nested_optional: str | None = None
|
||||
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:76:27
|
||||
|
|
||||
74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
75 | # Nested Optional types should be flattened
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
| ^^^^^^^^^^^^^
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
73 73 |
|
||||
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 |-nested_optional: Optional[Optional[str]] = None
|
||||
76 |+nested_optional: Optional[str | None] = None
|
||||
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:77:25
|
||||
|
|
||||
75 | # Nested Optional types should be flattened
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 |-nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
77 |+nested_optional_typing: int | None = None
|
||||
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:77:41
|
||||
|
|
||||
75 | # Nested Optional types should be flattened
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
| ^^^^^^^^^^^^^
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 |-nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
77 |+nested_optional_typing: typing.Optional[int | None] = None
|
||||
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:78:25
|
||||
|
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 |-triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
78 |+triple_nested_optional: str | None = None
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:78:34
|
||||
|
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 |-triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
78 |+triple_nested_optional: Optional[str | None] = None
|
||||
|
||||
UP045 [*] Use `X | None` for type annotations
|
||||
--> UP045.py:78:43
|
||||
|
|
||||
76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to `X | None`
|
||||
|
||||
ℹ Safe fix
|
||||
75 75 | # Nested Optional types should be flattened
|
||||
76 76 | nested_optional: Optional[Optional[str]] = None
|
||||
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
78 |-triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
78 |+triple_nested_optional: Optional[Optional[str | None]] = None
|
||||
|
||||
@@ -9,7 +9,7 @@ use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, Span};
|
||||
use ruff_notebook::Notebook;
|
||||
#[cfg(not(fuzzing))]
|
||||
use ruff_notebook::NotebookError;
|
||||
@@ -281,10 +281,16 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
|
||||
// noqa offset and the source file
|
||||
let range = diagnostic.expect_range();
|
||||
diagnostic.set_noqa_offset(directives.noqa_line_for.resolve(range.start()));
|
||||
if let Some(annotation) = diagnostic.primary_annotation_mut() {
|
||||
annotation.set_span(
|
||||
ruff_db::diagnostic::Span::from(source_code.clone()).with_range(range),
|
||||
);
|
||||
// This part actually is necessary to avoid long relative paths in snapshots.
|
||||
for annotation in diagnostic.annotations_mut() {
|
||||
let range = annotation.get_span().range().unwrap();
|
||||
annotation.set_span(Span::from(source_code.clone()).with_range(range));
|
||||
}
|
||||
for sub in diagnostic.sub_diagnostics_mut() {
|
||||
for annotation in sub.annotations_mut() {
|
||||
let range = annotation.get_span().range().unwrap();
|
||||
annotation.set_span(Span::from(source_code.clone()).with_range(range));
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic
|
||||
|
||||
2
crates/ty/docs/configuration.md
generated
2
crates/ty/docs/configuration.md
generated
@@ -44,7 +44,7 @@ or pyright's `stubPath` configuration setting.
|
||||
|
||||
```toml
|
||||
[tool.ty.environment]
|
||||
extra-paths = ["~/shared/my-search-path"]
|
||||
extra-paths = ["./shared/my-search-path"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
225
crates/ty/docs/rules.md
generated
225
crates/ty/docs/rules.md
generated
@@ -36,7 +36,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L101)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L145)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -88,7 +88,7 @@ f(int) # error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L171)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -117,7 +117,7 @@ a = 1
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L196)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -147,7 +147,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L222)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -177,7 +177,7 @@ class B(A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L287)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L295)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -202,7 +202,7 @@ class B(A, A): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L308)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L519)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -334,7 +334,7 @@ class C(A, B): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L474)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L340)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L348)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -445,7 +445,7 @@ an atypical memory layout.
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L519)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L588)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L559)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -496,7 +496,7 @@ a: int = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -523,12 +523,46 @@ C().instance_var = 3 # okay
|
||||
C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
```
|
||||
|
||||
## `invalid-await`
|
||||
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for `await` being used with types that are not [Awaitable].
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Such expressions will lead to `TypeError` being raised at runtime.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
class InvalidAwait:
|
||||
def __await__(self) -> int:
|
||||
return 5
|
||||
|
||||
async def main() -> None:
|
||||
await InvalidAwait() # error: [invalid-await]
|
||||
await 42 # error: [invalid-await]
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
[Awaitable]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Awaitable
|
||||
|
||||
## `invalid-base`
|
||||
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L581)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L680)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -550,7 +584,7 @@ class A(42): ... # error: [invalid-base]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L731)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -575,7 +609,7 @@ with 1:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L653)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L752)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -602,7 +636,7 @@ a: str
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L676)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L775)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -644,7 +678,7 @@ except ZeroDivisionError:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -675,7 +709,7 @@ class C[U](Generic[T]): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L494)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L563)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -704,7 +738,7 @@ alice["height"] # KeyError: 'height'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L738)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L837)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -737,7 +771,7 @@ def f(t: TypeVar("U")): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L787)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -764,12 +798,42 @@ class B(metaclass=f): ...
|
||||
|
||||
- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
|
||||
|
||||
## `invalid-named-tuple`
|
||||
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for invalidly defined `NamedTuple` classes.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
An invalidly defined `NamedTuple` class may lead to the type checker
|
||||
drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
|
||||
|
||||
**Examples**
|
||||
|
||||
A class definition cannot combine `NamedTuple` with other base classes
|
||||
in multiple inheritance; doing so raises a `TypeError` at runtime. The sole
|
||||
exception to this rule is `Generic[]`, which can be used alongside `NamedTuple`
|
||||
in a class's bases list.
|
||||
|
||||
```pycon
|
||||
>>> from typing import NamedTuple
|
||||
>>> class Foo(NamedTuple, object): ...
|
||||
TypeError: can only inherit from a NamedTuple type and Generic
|
||||
```
|
||||
|
||||
## `invalid-overload`
|
||||
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L814)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L913)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -817,7 +881,7 @@ def foo(x: int) -> int: ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L857)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -841,12 +905,12 @@ def f(a: int = ''): ...
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L422)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for invalidly defined protocol classes.
|
||||
Checks for protocol classes that will raise `TypeError` at runtime.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
@@ -873,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L877)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L976)
|
||||
</small>
|
||||
|
||||
Checks for `raise` statements that raise non-exceptions or use invalid
|
||||
@@ -920,7 +984,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L540)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L609)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -943,7 +1007,7 @@ def func() -> int:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L920)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -997,7 +1061,7 @@ TODO #14889
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1022,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1058)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1050,7 +1114,7 @@ TYPE_CHECKING = ''
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1078,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1035)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1110,7 +1174,7 @@ f(10) # Error
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1007)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1106)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1142,7 +1206,7 @@ class C:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1162)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1175,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1092)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1191)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1198,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1111)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1210)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1225,7 +1289,7 @@ func("string") # error: [no-matching-overload]
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1247,7 +1311,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1152)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1271,7 +1335,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1203)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1325,7 +1389,7 @@ def test(): -> "int":
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1539)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1638)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1353,7 +1417,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1294)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1393)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1380,7 +1444,7 @@ class B(A): ... # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1339)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1438)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1405,7 +1469,7 @@ f("foo") # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1317)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1416)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1431,7 +1495,7 @@ def _(x: int):
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1360)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1459)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1475,7 +1539,7 @@ class A:
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1500,7 +1564,7 @@ f(x=1, y=2) # Error raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1438)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1526,7 +1590,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1460)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1559)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1549,7 +1613,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1479)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1578)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1572,7 +1636,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1172)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1607,7 +1671,7 @@ b1 < b2 < b1 # exception raised here
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1597)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1633,7 +1697,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
<small>
|
||||
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1619)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1651,12 +1715,51 @@ l = list(range(10))
|
||||
l[1:10:0] # ValueError: slice step cannot be zero
|
||||
```
|
||||
|
||||
## `ambiguous-protocol-member`
|
||||
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L458)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for protocol classes with members that will lead to ambiguous interfaces.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Assigning to an undeclared variable in a protocol class leads to an ambiguous
|
||||
interface which may lead to the type checker inferring unexpected things. It's
|
||||
recommended to ensure that all members of a protocol class are explicitly declared.
|
||||
|
||||
**Examples**
|
||||
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
|
||||
class BaseProto(Protocol):
|
||||
a: int # fine (explicitly declared as `int`)
|
||||
def method_member(self) -> int: ... # fine: a method definition using `def` is considered a declaration
|
||||
c = "some variable" # error: no explicit declaration, leading to ambiguity
|
||||
b = method_member # error: no explicit declaration, leading to ambiguity
|
||||
|
||||
# error: this creates implicit assignments of `d` and `e` in the protocol class body.
|
||||
# Were they really meant to be considered protocol members?
|
||||
for d, e in enumerate(range(42)):
|
||||
pass
|
||||
|
||||
class SubProto(BaseProto, Protocol):
|
||||
a = 42 # fine (declared in superclass)
|
||||
```
|
||||
|
||||
## `deprecated`
|
||||
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L266)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L274)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1709,7 +1812,7 @@ a = 20 / 0 # type: ignore
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1735,7 +1838,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L119)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L127)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1765,7 +1868,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1345)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1795,7 +1898,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1591)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1690)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1820,7 +1923,7 @@ cast(int, f()) # Redundant
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1399)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1871,7 +1974,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1612)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1711)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1925,7 +2028,7 @@ def g():
|
||||
<small>
|
||||
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L599)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L698)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1962,7 +2065,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L248)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
@@ -1984,7 +2087,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
<small>
|
||||
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
|
||||
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272)
|
||||
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371)
|
||||
</small>
|
||||
|
||||
**What it does**
|
||||
|
||||
@@ -62,6 +62,10 @@ pub(crate) fn version() -> Result<()> {
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
// Enabled ANSI colors on Windows 10.
|
||||
#[cfg(windows)]
|
||||
assert!(colored::control::set_virtual_terminal(true).is_ok());
|
||||
|
||||
set_colored_override(args.color);
|
||||
|
||||
let verbosity = args.verbosity.level();
|
||||
|
||||
@@ -345,6 +345,51 @@ import bar",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// On Unix systems, a virtual environment can come with multiple `site-packages` directories:
|
||||
/// one at `<sys.prefix>/lib/pythonX.Y/site-packages` and one at
|
||||
/// `<sys.prefix>/lib64/pythonX.Y/site-packages`. According to [the stdlib docs], the `lib64`
|
||||
/// is not *meant* to have any Python files in it (only C extensions and similar). Empirically,
|
||||
/// however, it sometimes does indeed have Python files in it: popular tools such as poetry
|
||||
/// appear to sometimes install Python packages into the `lib64` site-packages directory even
|
||||
/// though they probably shouldn't. We therefore check for both a `lib64` and a `lib` directory,
|
||||
/// and add them both as search paths if they both exist.
|
||||
///
|
||||
/// See:
|
||||
/// - <https://github.com/astral-sh/ty/issues/1043>
|
||||
/// - <https://github.com/astral-sh/ty/issues/257>.
|
||||
///
|
||||
/// [the stdlib docs]: https://docs.python.org/3/library/sys.html#sys.platlibdir
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(".venv/lib/python3.13/site-packages/foo.py", ""),
|
||||
(".venv/lib64/python3.13/site-packages/bar.py", ""),
|
||||
("test.py", "import foo, bar, baz"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python").arg(".venv"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `baz`
|
||||
--> test.py:1:18
|
||||
|
|
||||
1 | import foo, bar, baz
|
||||
| ^^^
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
@@ -762,7 +807,7 @@ fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: Failed to discover the site-packages directory
|
||||
Cause: Failed to iterate over the contents of the `lib` directory of the Python installation
|
||||
Cause: Failed to iterate over the contents of the `lib`/`lib64` directories of the Python installation
|
||||
|
||||
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
||||
|
|
||||
@@ -771,8 +816,6 @@ fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
|
||||
3 | python = "directory-but-no-site-packages"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
Cause: No such file or directory (os error 2)
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
@@ -857,59 +900,165 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
/// The `site-packages` directory is used by ty for external import.
|
||||
/// Ty does the following checks to discover the `site-packages` directory in the order:
|
||||
/// 1) If `VIRTUAL_ENV` environment variable is set
|
||||
/// 2) If `CONDA_PREFIX` environment variable is set
|
||||
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
|
||||
/// 3) If a `.venv` directory exists at the project root
|
||||
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
|
||||
///
|
||||
/// This test is aiming at validating the logic around `CONDA_PREFIX`.
|
||||
/// This test (and the next one) is aiming at validating the logic around these cases.
|
||||
///
|
||||
/// A conda-like environment file structure is used
|
||||
/// We test by first not setting the `CONDA_PREFIX` and expect a fail.
|
||||
/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass.
|
||||
/// To do this we create a program that has these 4 imports:
|
||||
///
|
||||
/// ```python
|
||||
/// from package1 import ActiveVenv
|
||||
/// from package1 import ChildConda
|
||||
/// from package1 import WorkingVenv
|
||||
/// from package1 import BaseConda
|
||||
/// ```
|
||||
///
|
||||
/// We then create 4 different copies of package1. Each copy defines all of these
|
||||
/// classes... except the one that describes it. Therefore we know we got e.g.
|
||||
/// the working venv if we get a diagnostic like this:
|
||||
///
|
||||
/// ```text
|
||||
/// Unresolved import
|
||||
/// 4 | from package1 import WorkingVenv
|
||||
/// | ^^^^^^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// This test uses a directory structure as follows:
|
||||
///
|
||||
/// ├── project
|
||||
/// │ └── test.py
|
||||
/// └── conda-env
|
||||
/// └── lib
|
||||
/// └── python3.13
|
||||
/// └── site-packages
|
||||
/// └── package1
|
||||
/// └── __init__.py
|
||||
/// │ ├── test.py
|
||||
/// │ └── .venv
|
||||
/// │ ├── pyvenv.cfg
|
||||
/// │ └── lib
|
||||
/// │ └── python3.13
|
||||
/// │ └── site-packages
|
||||
/// │ └── package1
|
||||
/// │ └── __init__.py
|
||||
/// ├── myvenv
|
||||
/// │ ├── pyvenv.cfg
|
||||
/// │ └── lib
|
||||
/// │ └── python3.13
|
||||
/// │ └── site-packages
|
||||
/// │ └── package1
|
||||
/// │ └── __init__.py
|
||||
/// ├── conda-env
|
||||
/// │ └── lib
|
||||
/// │ └── python3.13
|
||||
/// │ └── site-packages
|
||||
/// │ └── package1
|
||||
/// │ └── __init__.py
|
||||
/// └── conda
|
||||
/// └── envs
|
||||
/// └── base
|
||||
/// └── lib
|
||||
/// └── python3.13
|
||||
/// └── site-packages
|
||||
/// └── package1
|
||||
/// └── __init__.py
|
||||
///
|
||||
/// test.py imports package1
|
||||
/// And the command is run in the `child` directory.
|
||||
#[test]
|
||||
fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
|
||||
let conda_package1_path = if cfg!(windows) {
|
||||
fn check_venv_resolution_with_working_venv() -> anyhow::Result<()> {
|
||||
let child_conda_package1_path = if cfg!(windows) {
|
||||
"conda-env/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let base_conda_package1_path = if cfg!(windows) {
|
||||
"conda/envs/base/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let working_venv_package1_path = if cfg!(windows) {
|
||||
"project/.venv/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"project/.venv/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let active_venv_package1_path = if cfg!(windows) {
|
||||
"myvenv/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/test.py",
|
||||
r#"
|
||||
import package1
|
||||
from package1 import ActiveVenv
|
||||
from package1 import ChildConda
|
||||
from package1 import WorkingVenv
|
||||
from package1 import BaseConda
|
||||
"#,
|
||||
),
|
||||
(
|
||||
conda_package1_path,
|
||||
"project/.venv/pyvenv.cfg",
|
||||
r#"
|
||||
home = ./
|
||||
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"myvenv/pyvenv.cfg",
|
||||
r#"
|
||||
home = ./
|
||||
|
||||
"#,
|
||||
),
|
||||
(
|
||||
active_venv_package1_path,
|
||||
r#"
|
||||
class ChildConda: ...
|
||||
class WorkingVenv: ...
|
||||
class BaseConda: ...
|
||||
"#,
|
||||
),
|
||||
(
|
||||
child_conda_package1_path,
|
||||
r#"
|
||||
class ActiveVenv: ...
|
||||
class WorkingVenv: ...
|
||||
class BaseConda: ...
|
||||
"#,
|
||||
),
|
||||
(
|
||||
working_venv_package1_path,
|
||||
r#"
|
||||
class ActiveVenv: ...
|
||||
class ChildConda: ...
|
||||
class BaseConda: ...
|
||||
"#,
|
||||
),
|
||||
(
|
||||
base_conda_package1_path,
|
||||
r#"
|
||||
class ActiveVenv: ...
|
||||
class ChildConda: ...
|
||||
class WorkingVenv: ...
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r"
|
||||
// Run with nothing set, should find the working venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:2:8
|
||||
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
|
||||
--> test.py:4:22
|
||||
|
|
||||
2 | import package1
|
||||
| ^^^^^^^^
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
| ^^^^^^^^^^^
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -918,12 +1067,373 @@ fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// do command : CONDA_PREFIX=<temp_dir>/conda_env
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
// Run with VIRTUAL_ENV set, should find the active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
|
||||
--> test.py:2:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX set, should find the child conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
|
||||
// should find child active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "base")
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
|
||||
--> test.py:2:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find working venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
|
||||
--> test.py:4:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
| ^^^^^^^^^^^
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The exact same test as above, but without a working venv
|
||||
///
|
||||
/// In this case the Base Conda should be a possible outcome.
|
||||
#[test]
|
||||
fn check_venv_resolution_without_working_venv() -> anyhow::Result<()> {
|
||||
let child_conda_package1_path = if cfg!(windows) {
|
||||
"conda-env/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let base_conda_package1_path = if cfg!(windows) {
|
||||
"conda/envs/base/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let active_venv_package1_path = if cfg!(windows) {
|
||||
"myvenv/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/test.py",
|
||||
r#"
|
||||
from package1 import ActiveVenv
|
||||
from package1 import ChildConda
|
||||
from package1 import WorkingVenv
|
||||
from package1 import BaseConda
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"myvenv/pyvenv.cfg",
|
||||
r#"
|
||||
home = ./
|
||||
|
||||
"#,
|
||||
),
|
||||
(
|
||||
active_venv_package1_path,
|
||||
r#"
|
||||
class ChildConda: ...
|
||||
class WorkingVenv: ...
|
||||
class BaseConda: ...
|
||||
"#,
|
||||
),
|
||||
(
|
||||
child_conda_package1_path,
|
||||
r#"
|
||||
class ActiveVenv: ...
|
||||
class WorkingVenv: ...
|
||||
class BaseConda: ...
|
||||
"#,
|
||||
),
|
||||
(
|
||||
base_conda_package1_path,
|
||||
r#"
|
||||
class ActiveVenv: ...
|
||||
class ChildConda: ...
|
||||
class WorkingVenv: ...
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Run with nothing set, should fail to find anything
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:2:6
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:3:6
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:4:6
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
| ^^^^^^^^
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `package1`
|
||||
--> test.py:5:6
|
||||
|
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
| ^^^^^^^^
|
||||
|
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 4 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// Run with VIRTUAL_ENV set, should find the active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
|
||||
--> test.py:2:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX set, should find the child conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ChildConda`
|
||||
--> test.py:3:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
3 | from package1 import ChildConda
|
||||
| ^^^^^^^^^^
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
|
||||
// should find child active venv
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda-env"))
|
||||
.env("CONDA_DEFAULT_ENV", "base")
|
||||
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
|
||||
--> test.py:2:22
|
||||
|
|
||||
2 | from package1 import ActiveVenv
|
||||
| ^^^^^^^^^^
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find base conda
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
|
||||
.env("CONDA_DEFAULT_ENV", "base"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-import]: Module `package1` has no member `BaseConda`
|
||||
--> test.py:5:22
|
||||
|
|
||||
3 | from package1 import ChildConda
|
||||
4 | from package1 import WorkingVenv
|
||||
5 | from package1 import BaseConda
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
|
||||
@@ -23,7 +23,18 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion<'
|
||||
let model = SemanticModel::new(db, file);
|
||||
let mut completions = match target {
|
||||
CompletionTargetAst::ObjectDot { expr } => model.attribute_completions(expr),
|
||||
CompletionTargetAst::ImportFrom { import, name } => model.import_completions(import, name),
|
||||
CompletionTargetAst::ObjectDotInImport { import, name } => {
|
||||
model.import_submodule_completions(import, name)
|
||||
}
|
||||
CompletionTargetAst::ObjectDotInImportFrom { import } => {
|
||||
model.from_import_submodule_completions(import)
|
||||
}
|
||||
CompletionTargetAst::ImportFrom { import, name } => {
|
||||
model.from_import_completions(import, name)
|
||||
}
|
||||
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
|
||||
model.import_completions()
|
||||
}
|
||||
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
|
||||
};
|
||||
completions.sort_by(compare_suggestions);
|
||||
@@ -50,11 +61,11 @@ enum CompletionTargetTokens<'t> {
|
||||
object: &'t Token,
|
||||
/// The token, if non-empty, following the dot.
|
||||
///
|
||||
/// This is currently unused, but we should use this
|
||||
/// eventually to remove completions that aren't a
|
||||
/// prefix of what has already been typed. (We are
|
||||
/// currently relying on the LSP client to do this.)
|
||||
#[expect(dead_code)]
|
||||
/// For right now, this is only used to determine which
|
||||
/// module in an `import` statement to return submodule
|
||||
/// completions for. But we could use it for other things,
|
||||
/// like only returning completions that start with a prefix
|
||||
/// corresponding to this token.
|
||||
attribute: Option<&'t Token>,
|
||||
},
|
||||
/// A `from module import attribute` token form was found, where
|
||||
@@ -63,6 +74,20 @@ enum CompletionTargetTokens<'t> {
|
||||
/// The module being imported from.
|
||||
module: &'t Token,
|
||||
},
|
||||
/// A `import module` token form was found, where `module` may be
|
||||
/// empty.
|
||||
Import {
|
||||
/// The token corresponding to the `import` keyword.
|
||||
import: &'t Token,
|
||||
/// The token closest to the cursor.
|
||||
///
|
||||
/// This is currently unused, but we should use this
|
||||
/// eventually to remove completions that aren't a
|
||||
/// prefix of what has already been typed. (We are
|
||||
/// currently relying on the LSP client to do this.)
|
||||
#[expect(dead_code)]
|
||||
module: &'t Token,
|
||||
},
|
||||
/// A token was found under the cursor, but it didn't
|
||||
/// match any of our anticipated token patterns.
|
||||
Generic { token: &'t Token },
|
||||
@@ -105,6 +130,8 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||
}
|
||||
} else if let Some(module) = import_from_tokens(before) {
|
||||
CompletionTargetTokens::ImportFrom { module }
|
||||
} else if let Some((import, module)) = import_tokens(before) {
|
||||
CompletionTargetTokens::Import { import, module }
|
||||
} else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Float]) {
|
||||
// If we're writing a `float`, then we should
|
||||
// specifically not offer completions. This wouldn't
|
||||
@@ -140,19 +167,47 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||
offset: TextSize,
|
||||
) -> Option<CompletionTargetAst<'t>> {
|
||||
match *self {
|
||||
CompletionTargetTokens::PossibleObjectDot { object, .. } => {
|
||||
CompletionTargetTokens::PossibleObjectDot { object, attribute } => {
|
||||
let covering_node = covering_node(parsed.syntax().into(), object.range())
|
||||
// We require that the end of the node range not
|
||||
// exceed the cursor offset. This avoids selecting
|
||||
// a node "too high" in the AST in cases where
|
||||
// completions are requested in the middle of an
|
||||
// expression. e.g., `foo.<CURSOR>.bar`.
|
||||
.find_last(|node| node.is_expr_attribute() && node.range().end() <= offset)
|
||||
.find_last(|node| {
|
||||
// We require that the end of the node range not
|
||||
// exceed the cursor offset. This avoids selecting
|
||||
// a node "too high" in the AST in cases where
|
||||
// completions are requested in the middle of an
|
||||
// expression. e.g., `foo.<CURSOR>.bar`.
|
||||
if node.is_expr_attribute() {
|
||||
return node.range().end() <= offset;
|
||||
}
|
||||
// For import statements though, they can't be
|
||||
// nested, so we don't care as much about the
|
||||
// cursor being strictly after the statement.
|
||||
// And indeed, sometimes it won't be! e.g.,
|
||||
//
|
||||
// import re, os.p<CURSOR>, zlib
|
||||
//
|
||||
// So just return once we find an import.
|
||||
node.is_stmt_import() || node.is_stmt_import_from()
|
||||
})
|
||||
.ok()?;
|
||||
match covering_node.node() {
|
||||
ast::AnyNodeRef::ExprAttribute(expr) => {
|
||||
Some(CompletionTargetAst::ObjectDot { expr })
|
||||
}
|
||||
ast::AnyNodeRef::StmtImport(import) => {
|
||||
let range = attribute
|
||||
.map(Ranged::range)
|
||||
.unwrap_or_else(|| object.range());
|
||||
// Find the name that overlaps with the
|
||||
// token we identified for the attribute.
|
||||
let name = import
|
||||
.names
|
||||
.iter()
|
||||
.position(|alias| alias.range().contains_range(range))?;
|
||||
Some(CompletionTargetAst::ObjectDotInImport { import, name })
|
||||
}
|
||||
ast::AnyNodeRef::StmtImportFrom(import) => {
|
||||
Some(CompletionTargetAst::ObjectDotInImportFrom { import })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -165,6 +220,20 @@ impl<'t> CompletionTargetTokens<'t> {
|
||||
};
|
||||
Some(CompletionTargetAst::ImportFrom { import, name: None })
|
||||
}
|
||||
CompletionTargetTokens::Import { import, .. } => {
|
||||
let covering_node = covering_node(parsed.syntax().into(), import.range())
|
||||
.find_first(|node| node.is_stmt_import() || node.is_stmt_import_from())
|
||||
.ok()?;
|
||||
match covering_node.node() {
|
||||
ast::AnyNodeRef::StmtImport(import) => {
|
||||
Some(CompletionTargetAst::Import { import, name: None })
|
||||
}
|
||||
ast::AnyNodeRef::StmtImportFrom(import) => {
|
||||
Some(CompletionTargetAst::ImportViaFrom { import })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
CompletionTargetTokens::Generic { token } => {
|
||||
let covering_node = covering_node(parsed.syntax().into(), token.range());
|
||||
Some(CompletionTargetAst::Scoped {
|
||||
@@ -188,6 +257,18 @@ enum CompletionTargetAst<'t> {
|
||||
/// A `object.attribute` scenario, where we want to
|
||||
/// list attributes on `object` for completions.
|
||||
ObjectDot { expr: &'t ast::ExprAttribute },
|
||||
/// A `import module.submodule` scenario, where we only want to
|
||||
/// list submodules for completions.
|
||||
ObjectDotInImport {
|
||||
/// The import statement.
|
||||
import: &'t ast::StmtImport,
|
||||
/// An index into `import.names`. The index is guaranteed to be
|
||||
/// valid.
|
||||
name: usize,
|
||||
},
|
||||
/// A `from module.submodule` scenario, where we only want to list
|
||||
/// submodules for completions.
|
||||
ObjectDotInImportFrom { import: &'t ast::StmtImportFrom },
|
||||
/// A `from module import attribute` scenario, where we want to
|
||||
/// list attributes on `module` for completions.
|
||||
ImportFrom {
|
||||
@@ -197,6 +278,24 @@ enum CompletionTargetAst<'t> {
|
||||
/// set, the index is guaranteed to be valid.
|
||||
name: Option<usize>,
|
||||
},
|
||||
/// A `import module` scenario, where we want to
|
||||
/// list available modules for completions.
|
||||
Import {
|
||||
/// The import statement.
|
||||
#[expect(dead_code)]
|
||||
import: &'t ast::StmtImport,
|
||||
/// An index into `import.names` if relevant. When this is
|
||||
/// set, the index is guaranteed to be valid.
|
||||
#[expect(dead_code)]
|
||||
name: Option<usize>,
|
||||
},
|
||||
/// A `from module` scenario, where we want to
|
||||
/// list available modules for completions.
|
||||
ImportViaFrom {
|
||||
/// The import statement.
|
||||
#[expect(dead_code)]
|
||||
import: &'t ast::StmtImportFrom,
|
||||
},
|
||||
/// A scoped scenario, where we want to list all items available in
|
||||
/// the most narrow scope containing the giving AST node.
|
||||
Scoped { node: ast::AnyNodeRef<'t> },
|
||||
@@ -317,6 +416,52 @@ fn import_from_tokens(tokens: &[Token]) -> Option<&Token> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Looks for the start of a `import <CURSOR>` statement.
|
||||
///
|
||||
/// This also handles cases like `import foo, c<CURSOR>, bar`.
|
||||
///
|
||||
/// If found, a token corresponding to the `import` or `from` keyword
|
||||
/// and the the closest point of the `<CURSOR>` is returned.
|
||||
///
|
||||
/// It is assumed that callers will call `from_import_tokens` first to
|
||||
/// try and recognize a `from ... import ...` statement before using
|
||||
/// this.
|
||||
fn import_tokens(tokens: &[Token]) -> Option<(&Token, &Token)> {
|
||||
use TokenKind as TK;
|
||||
|
||||
/// A look-back limit, in order to bound work.
|
||||
///
|
||||
/// See `LIMIT` in `import_from_tokens` for more context.
|
||||
const LIMIT: usize = 1_000;
|
||||
|
||||
/// A state used to "parse" the tokens preceding the user's cursor,
|
||||
/// in reverse, to detect a `import` statement.
|
||||
enum S {
|
||||
Start,
|
||||
Names,
|
||||
}
|
||||
|
||||
let mut state = S::Start;
|
||||
let module_token = tokens.last()?;
|
||||
// Move backward through the tokens until we get to
|
||||
// the `import` token.
|
||||
for token in tokens.iter().rev().take(LIMIT) {
|
||||
state = match (state, token.kind()) {
|
||||
// It's okay to pop off a newline token here initially,
|
||||
// since it may occur when the name being imported is
|
||||
// empty.
|
||||
(S::Start, TK::Newline) => S::Names,
|
||||
// Munch through tokens that can make up an alias.
|
||||
(S::Start | S::Names, TK::Name | TK::Comma | TK::As | TK::Unknown) => S::Names,
|
||||
(S::Start | S::Names, TK::Import | TK::From) => {
|
||||
return Some((token, module_token));
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Order completions lexicographically, with these exceptions:
|
||||
///
|
||||
/// 1) A `_[^_]` prefix sorts last and
|
||||
@@ -1247,7 +1392,7 @@ quux.<CURSOR>
|
||||
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method Quux.__new__() -> Self@__new__
|
||||
__new__ :: bound method Quux.__new__() -> Quux
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
@@ -1292,7 +1437,7 @@ quux.b<CURSOR>
|
||||
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: bound method Quux.__new__() -> Self@__new__
|
||||
__new__ :: bound method Quux.__new__() -> Quux
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
@@ -2262,7 +2407,11 @@ import fo<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
// This snapshot would generate a big list of modules,
|
||||
// which is kind of annoying. So just assert that it
|
||||
// runs without panicking and produces some non-empty
|
||||
// output.
|
||||
assert!(!test.completions_without_builtins().is_empty());
|
||||
}
|
||||
|
||||
// Ref: https://github.com/astral-sh/ty/issues/572
|
||||
@@ -2274,7 +2423,11 @@ import foo as ba<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
// This snapshot would generate a big list of modules,
|
||||
// which is kind of annoying. So just assert that it
|
||||
// runs without panicking and produces some non-empty
|
||||
// output.
|
||||
assert!(!test.completions_without_builtins().is_empty());
|
||||
}
|
||||
|
||||
// Ref: https://github.com/astral-sh/ty/issues/572
|
||||
@@ -2286,7 +2439,11 @@ from fo<CURSOR> import wat
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
|
||||
// This snapshot would generate a big list of modules,
|
||||
// which is kind of annoying. So just assert that it
|
||||
// runs without panicking and produces some non-empty
|
||||
// output.
|
||||
assert!(!test.completions_without_builtins().is_empty());
|
||||
}
|
||||
|
||||
// Ref: https://github.com/astral-sh/ty/issues/572
|
||||
@@ -2697,6 +2854,143 @@ importlib.<CURSOR>
|
||||
test.assert_completions_include("resources");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_with_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import c<CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_without_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import <CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_multiple() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import re, c<CURSOR>, sys
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_with_aliases() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import re as regexp, c<CURSOR>, sys as system
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_over_multiple_lines() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import re as regexp, \\
|
||||
c<CURSOR>, \\
|
||||
sys as system
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_unknown_in_module() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import ?, <CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_via_from_with_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
from c<CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_via_from_without_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
from <CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("collections");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_statement_with_submodule_with_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import os.p<CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("path");
|
||||
test.assert_completions_do_not_include("abspath");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_statement_with_submodule_multiple() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import re, os.p<CURSOR>, zlib
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("path");
|
||||
test.assert_completions_do_not_include("abspath");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_statement_with_submodule_without_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
import os.<CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("path");
|
||||
test.assert_completions_do_not_include("abspath");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_via_from_with_submodule_with_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
from os.p<CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("path");
|
||||
test.assert_completions_do_not_include("abspath");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_via_from_with_submodule_without_leading_character() {
|
||||
let test = cursor_test(
|
||||
"\
|
||||
from os.<CURSOR>
|
||||
",
|
||||
);
|
||||
test.assert_completions_include("path");
|
||||
test.assert_completions_do_not_include("abspath");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regression_test_issue_642() {
|
||||
// Regression test for https://github.com/astral-sh/ty/issues/642
|
||||
|
||||
@@ -11,6 +11,7 @@ use ruff_db::parsed::ParsedModuleRef;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use ty_python_semantic::HasDefinition;
|
||||
use ty_python_semantic::ImportAliasResolution;
|
||||
use ty_python_semantic::ResolvedDefinition;
|
||||
use ty_python_semantic::types::Type;
|
||||
@@ -285,36 +286,24 @@ impl GotoTarget<'_> {
|
||||
|
||||
// For already-defined symbols, they are their own definitions
|
||||
GotoTarget::FunctionDef(function) => {
|
||||
let range = function.name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: function.range(),
|
||||
}),
|
||||
))
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(function.definition(&model)),
|
||||
]))
|
||||
}
|
||||
|
||||
GotoTarget::ClassDef(class) => {
|
||||
let range = class.name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: class.range(),
|
||||
}),
|
||||
))
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(class.definition(&model)),
|
||||
]))
|
||||
}
|
||||
|
||||
GotoTarget::Parameter(parameter) => {
|
||||
let range = parameter.name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: parameter.range(),
|
||||
}),
|
||||
))
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(parameter.definition(&model)),
|
||||
]))
|
||||
}
|
||||
|
||||
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
|
||||
@@ -376,14 +365,10 @@ impl GotoTarget<'_> {
|
||||
|
||||
// For exception variables, they are their own definitions (like parameters)
|
||||
GotoTarget::ExceptVariable(except_handler) => {
|
||||
if let Some(name) = &except_handler.name {
|
||||
let range = name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let model = SemanticModel::new(db, file);
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(except_handler.definition(&model)),
|
||||
]))
|
||||
}
|
||||
|
||||
// For pattern match rest variables, they are their own definitions
|
||||
|
||||
@@ -181,6 +181,49 @@ def other_function(): ...
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a function definition in a .pyi should go to the .py
|
||||
#[test]
|
||||
fn goto_definition_stub_map_function_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
|
||||
def other_function():
|
||||
return "other"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_fun<CURSOR>ction(): ...
|
||||
|
||||
def other_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:5
|
||||
|
|
||||
2 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> mymodule.pyi:2:5
|
||||
|
|
||||
2 | def my_function(): ...
|
||||
| ^^^^^^^^^^^
|
||||
3 |
|
||||
4 | def other_function(): ...
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a function that's redefined many times in the impl .py
|
||||
///
|
||||
/// Currently this yields all instances. There's an argument for only yielding
|
||||
@@ -328,6 +371,53 @@ class MyOtherClass:
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class def in a .pyi should go to the .py
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyCl<CURSOR>ass:
|
||||
def __init__(self, val: bool): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
|
|
||||
info: Source
|
||||
--> mymodule.pyi:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self, val: bool): ...
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class init should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_init() {
|
||||
|
||||
@@ -405,9 +405,7 @@ except ValueError as err:
|
||||
",
|
||||
);
|
||||
|
||||
// Note: Currently only finds the declaration, not the usages
|
||||
// This is because semantic analysis for except handler variables isn't fully implemented
|
||||
assert_snapshot!(test.references(), @r###"
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:29
|
||||
|
|
||||
@@ -418,7 +416,37 @@ except ValueError as err:
|
||||
5 | print(f'Error: {err}')
|
||||
6 | return err
|
||||
|
|
||||
"###);
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:21
|
||||
|
|
||||
3 | x = 1 / 0
|
||||
4 | except ZeroDivisionError as err:
|
||||
5 | print(f'Error: {err}')
|
||||
| ^^^
|
||||
6 | return err
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:6:12
|
||||
|
|
||||
4 | except ZeroDivisionError as err:
|
||||
5 | print(f'Error: {err}')
|
||||
6 | return err
|
||||
| ^^^
|
||||
7 |
|
||||
8 | try:
|
||||
|
|
||||
|
||||
info[references]: Reference 4
|
||||
--> main.py:11:31
|
||||
|
|
||||
9 | y = 2 / 0
|
||||
10 | except ValueError as err:
|
||||
11 | print(f'Different error: {err}')
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -197,16 +197,16 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:901:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
900 |
|
||||
901 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
902 | """str(object='') -> str
|
||||
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
4 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
@@ -226,16 +226,16 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:901:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
900 |
|
||||
901 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
902 | """str(object='') -> str
|
||||
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
@@ -243,7 +243,7 @@ mod tests {
|
||||
2 | a: str = "test"
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -342,16 +342,16 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:901:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
900 |
|
||||
901 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
902 | """str(object='') -> str
|
||||
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
@@ -361,7 +361,7 @@ mod tests {
|
||||
4 | test(a= "123")
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -411,16 +411,16 @@ f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:2890:7
|
||||
--> stdlib/builtins.pyi:2901:7
|
||||
|
|
||||
2888 | """See PEP 585"""
|
||||
2889 |
|
||||
2890 | class dict(MutableMapping[_KT, _VT]):
|
||||
2899 | """See PEP 585"""
|
||||
2900 |
|
||||
2901 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
2891 | """dict() -> new empty dictionary
|
||||
2892 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
2902 | """dict() -> new empty dictionary
|
||||
2903 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:5
|
||||
@@ -430,7 +430,7 @@ f(**kwargs<CURSOR>)
|
||||
6 | f(**kwargs)
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -442,16 +442,16 @@ f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:901:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
900 |
|
||||
901 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
902 | """str(object='') -> str
|
||||
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -460,7 +460,7 @@ f(**kwargs<CURSOR>)
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -535,16 +535,16 @@ f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:901:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
900 |
|
||||
901 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
902 | """str(object='') -> str
|
||||
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:27
|
||||
@@ -554,7 +554,7 @@ f(**kwargs<CURSOR>)
|
||||
4 | print(a)
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -566,7 +566,7 @@ f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:922:11
|
||||
|
|
||||
@@ -585,14 +585,14 @@ f(**kwargs<CURSOR>)
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
--> stdlib/builtins.pyi:901:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
900 |
|
||||
901 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
902 | """str(object='') -> str
|
||||
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
@@ -601,7 +601,7 @@ f(**kwargs<CURSOR>)
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
"###);
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
|
||||
@@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
use ty_python_semantic::SemanticModel;
|
||||
use ty_python_semantic::types::Type;
|
||||
use ty_python_semantic::{DisplaySettings, SemanticModel};
|
||||
|
||||
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
@@ -135,7 +135,10 @@ impl fmt::Display for DisplayHoverContent<'_, '_> {
|
||||
match self.content {
|
||||
HoverContent::Type(ty) => self
|
||||
.kind
|
||||
.fenced_code_block(ty.display(self.db), "python")
|
||||
.fenced_code_block(
|
||||
ty.display_with(self.db, DisplaySettings::default().multiline()),
|
||||
"python",
|
||||
)
|
||||
.fmt(f),
|
||||
HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f),
|
||||
}
|
||||
@@ -201,7 +204,10 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
def my_func(a, b) -> Unknown
|
||||
def my_func(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
---------------------------------------------
|
||||
This is such a great func!!
|
||||
|
||||
@@ -211,7 +217,10 @@ mod tests {
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
def my_func(a, b) -> Unknown
|
||||
def my_func(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
```
|
||||
---
|
||||
```text
|
||||
@@ -237,6 +246,63 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_function_def() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_fu<CURSOR>nc(a, b):
|
||||
'''This is such a great func!!
|
||||
|
||||
Args:
|
||||
a: first for a reason
|
||||
b: coming for `a`'s title
|
||||
'''
|
||||
return 0
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
def my_func(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
---------------------------------------------
|
||||
This is such a great func!!
|
||||
|
||||
Args:
|
||||
a: first for a reason
|
||||
b: coming for `a`'s title
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
def my_func(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
```
|
||||
---
|
||||
```text
|
||||
This is such a great func!!
|
||||
|
||||
Args:
|
||||
a: first for a reason
|
||||
b: coming for `a`'s title
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | def my_func(a, b):
|
||||
| ^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 | '''This is such a great func!!
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_class() {
|
||||
let test = cursor_test(
|
||||
@@ -304,6 +370,71 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_class_def() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class MyCla<CURSOR>ss:
|
||||
'''
|
||||
This is such a great class!!
|
||||
|
||||
Don't you know?
|
||||
|
||||
Everyone loves my class!!
|
||||
|
||||
'''
|
||||
def __init__(self, val):
|
||||
"""initializes MyClass (perfectly)"""
|
||||
self.val = val
|
||||
|
||||
def my_method(self, a, b):
|
||||
'''This is such a great func!!
|
||||
|
||||
Args:
|
||||
a: first for a reason
|
||||
b: coming for `a`'s title
|
||||
'''
|
||||
return 0
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<class 'MyClass'>
|
||||
---------------------------------------------
|
||||
This is such a great class!!
|
||||
|
||||
Don't you know?
|
||||
|
||||
Everyone loves my class!!
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
```text
|
||||
This is such a great class!!
|
||||
|
||||
Don't you know?
|
||||
|
||||
Everyone loves my class!!
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:15
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 | '''
|
||||
4 | This is such a great class!!
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_class_init() {
|
||||
let test = cursor_test(
|
||||
@@ -403,7 +534,10 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
bound method MyClass.my_method(a, b) -> Unknown
|
||||
bound method MyClass.my_method(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
---------------------------------------------
|
||||
This is such a great func!!
|
||||
|
||||
@@ -413,7 +547,10 @@ mod tests {
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
bound method MyClass.my_method(a, b) -> Unknown
|
||||
bound method MyClass.my_method(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
```
|
||||
---
|
||||
```text
|
||||
@@ -485,10 +622,16 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
def foo(a, b) -> Unknown
|
||||
def foo(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
def foo(a, b) -> Unknown
|
||||
def foo(
|
||||
a,
|
||||
b
|
||||
) -> Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
@@ -571,6 +714,40 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_keyword_parameter_def() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a<CURSOR>b: int):
|
||||
"""my cool test
|
||||
|
||||
Args:
|
||||
ab: a nice little integer
|
||||
"""
|
||||
return 0
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
int
|
||||
---------------------------------------------
|
||||
```python
|
||||
int
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:22
|
||||
|
|
||||
2 | def test(ab: int):
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
3 | """my cool test
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_union() {
|
||||
let test = cursor_test(
|
||||
@@ -613,6 +790,128 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_overload() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def foo(a: int, b):
|
||||
"""The first overload"""
|
||||
return 0
|
||||
|
||||
@overload
|
||||
def foo(a: str, b):
|
||||
"""The second overload"""
|
||||
return 1
|
||||
|
||||
if random.choice([True, False]):
|
||||
a = 1
|
||||
else:
|
||||
a = "hello"
|
||||
|
||||
foo<CURSOR>(a, 2)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(
|
||||
a: int,
|
||||
b
|
||||
) -> Unknown
|
||||
(
|
||||
a: str,
|
||||
b
|
||||
) -> Unknown
|
||||
---------------------------------------------
|
||||
The first overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
a: int,
|
||||
b
|
||||
) -> Unknown
|
||||
(
|
||||
a: str,
|
||||
b
|
||||
) -> Unknown
|
||||
```
|
||||
---
|
||||
```text
|
||||
The first overload
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:19:13
|
||||
|
|
||||
17 | a = "hello"
|
||||
18 |
|
||||
19 | foo(a, 2)
|
||||
| ^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_overload_compact() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def foo(a: int):
|
||||
"""The first overload"""
|
||||
return 0
|
||||
|
||||
@overload
|
||||
def foo(a: str):
|
||||
"""The second overload"""
|
||||
return 1
|
||||
|
||||
if random.choice([True, False]):
|
||||
a = 1
|
||||
else:
|
||||
a = "hello"
|
||||
|
||||
foo<CURSOR>(a)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(a: int) -> Unknown
|
||||
(a: str) -> Unknown
|
||||
---------------------------------------------
|
||||
The first overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
(a: str) -> Unknown
|
||||
```
|
||||
---
|
||||
```text
|
||||
The first overload
|
||||
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:19:13
|
||||
|
|
||||
17 | a = "hello"
|
||||
18 |
|
||||
19 | foo(a)
|
||||
| ^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_module() {
|
||||
let mut test = cursor_test(
|
||||
@@ -1081,6 +1380,110 @@ mod tests {
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_complex_type1() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable, Any, List
|
||||
def ab(x: int, y: Callable[[int, int], Any], z: List[int]) -> int: ...
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
def ab(
|
||||
x: int,
|
||||
y: (int, int, /) -> Any,
|
||||
z: list[int]
|
||||
) -> int
|
||||
---------------------------------------------
|
||||
```python
|
||||
def ab(
|
||||
x: int,
|
||||
y: (int, int, /) -> Any,
|
||||
z: list[int]
|
||||
) -> int
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:9
|
||||
|
|
||||
3 | def ab(x: int, y: Callable[[int, int], Any], z: List[int]) -> int: ...
|
||||
4 |
|
||||
5 | ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_complex_type2() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable, Tuple, Any
|
||||
ab: Tuple[Any, int, Callable[[int, int], Any]] = ...
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
tuple[Any, int, (int, int, /) -> Any]
|
||||
---------------------------------------------
|
||||
```python
|
||||
tuple[Any, int, (int, int, /) -> Any]
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:9
|
||||
|
|
||||
3 | ab: Tuple[Any, int, Callable[[int, int], Any]] = ...
|
||||
4 |
|
||||
5 | ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_complex_type3() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable, Any
|
||||
ab: Callable[[int, int], Any] | None = ...
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
((int, int, /) -> Any) | None
|
||||
---------------------------------------------
|
||||
```python
|
||||
((int, int, /) -> Any) | None
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:9
|
||||
|
|
||||
3 | ab: Callable[[int, int], Any] | None = ...
|
||||
4 |
|
||||
5 | ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_docstring() {
|
||||
let test = cursor_test(
|
||||
|
||||
@@ -85,6 +85,13 @@ pub struct InlayHintSettings {
|
||||
/// foo("x="1)
|
||||
/// ```
|
||||
pub call_argument_names: bool,
|
||||
// Add any new setting that enables additional inlays to `any_enabled`.
|
||||
}
|
||||
|
||||
impl InlayHintSettings {
|
||||
pub fn any_enabled(&self) -> bool {
|
||||
self.variable_types || self.call_argument_names
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InlayHintSettings {
|
||||
|
||||
@@ -235,7 +235,8 @@ impl HasNavigationTargets for Type<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
Type::Union(union) => union
|
||||
.iter(db)
|
||||
.elements(db)
|
||||
.iter()
|
||||
.flat_map(|target| target.navigation_targets(db))
|
||||
.collect(),
|
||||
|
||||
|
||||
@@ -337,7 +337,9 @@ impl<'db> SemanticTokenVisitor<'db> {
|
||||
|
||||
match ty {
|
||||
Type::ClassLiteral(_) => (SemanticTokenType::Class, modifiers),
|
||||
Type::TypeVar(_) => (SemanticTokenType::TypeParameter, modifiers),
|
||||
Type::NonInferableTypeVar(_) | Type::TypeVar(_) => {
|
||||
(SemanticTokenType::TypeParameter, modifiers)
|
||||
}
|
||||
Type::FunctionLiteral(_) => {
|
||||
// Check if this is a method based on current scope
|
||||
if self.in_class_scope {
|
||||
|
||||
@@ -21,6 +21,8 @@ mod changes;
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb {
|
||||
fn project(&self) -> Project;
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Db>;
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -484,6 +486,10 @@ impl Db for ProjectDatabase {
|
||||
fn project(&self) -> Project {
|
||||
self.project.unwrap()
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Db> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "format")]
|
||||
@@ -611,6 +617,10 @@ pub(crate) mod tests {
|
||||
fn project(&self) -> Project {
|
||||
self.project.unwrap()
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Db> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -498,7 +498,7 @@ pub struct EnvironmentOptions {
|
||||
default = r#"[]"#,
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
extra-paths = ["~/shared/my-search-path"]
|
||||
extra-paths = ["./shared/my-search-path"]
|
||||
"#
|
||||
)]
|
||||
pub extra_paths: Option<Vec<RelativePathBuf>>,
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_db::files::{File, system_path_to_file};
|
||||
use ruff_db::system::walk_directory::{ErrorKind, WalkDirectoryBuilder, WalkState};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -163,20 +163,24 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
|
||||
/// Walks the project paths and collects the paths of all files that
|
||||
/// are included in the project.
|
||||
pub(crate) fn walk_paths(self) -> (Vec<SystemPathBuf>, Vec<IOErrorDiagnostic>) {
|
||||
let paths = std::sync::Mutex::new(Vec::new());
|
||||
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
|
||||
let files = std::sync::Mutex::new(Vec::new());
|
||||
let diagnostics = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
self.walker.run(|| {
|
||||
Box::new(|entry| {
|
||||
let db = db.dyn_clone();
|
||||
let filter = &self.filter;
|
||||
let files = &files;
|
||||
let diagnostics = &diagnostics;
|
||||
|
||||
Box::new(move |entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
// Skip excluded directories unless they were explicitly passed to the walker
|
||||
// (which is the case passed to `ty check <paths>`).
|
||||
if entry.file_type().is_directory() {
|
||||
if entry.depth() > 0 {
|
||||
let directory_included = self
|
||||
.filter
|
||||
let directory_included = filter
|
||||
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
|
||||
return match directory_included {
|
||||
IncludeResult::Included => WalkState::Continue,
|
||||
@@ -210,8 +214,7 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
// For all files, except the ones that were explicitly passed to the walker (CLI),
|
||||
// check if they're included in the project.
|
||||
if entry.depth() > 0 {
|
||||
match self
|
||||
.filter
|
||||
match filter
|
||||
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
|
||||
{
|
||||
IncludeResult::Included => {},
|
||||
@@ -232,8 +235,11 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut paths = paths.lock().unwrap();
|
||||
paths.push(entry.into_path());
|
||||
// If this returns `Err`, then the file was deleted between now and when the walk callback was called.
|
||||
// We can ignore this.
|
||||
if let Ok(file) = system_path_to_file(&*db, entry.path()) {
|
||||
files.lock().unwrap().push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => match error.kind() {
|
||||
@@ -274,39 +280,14 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
});
|
||||
|
||||
(
|
||||
paths.into_inner().unwrap(),
|
||||
files.into_inner().unwrap(),
|
||||
diagnostics.into_inner().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
|
||||
let (paths, diagnostics) = self.walk_paths();
|
||||
|
||||
(
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(move |path| {
|
||||
// If this returns `None`, then the file was deleted between the `walk_directory` call and now.
|
||||
// We can ignore this.
|
||||
system_path_to_file(db, &path).ok()
|
||||
})
|
||||
.collect(),
|
||||
diagnostics,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_set(self, db: &dyn Db) -> (FxHashSet<File>, Vec<IOErrorDiagnostic>) {
|
||||
let (paths, diagnostics) = self.walk_paths();
|
||||
|
||||
let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher);
|
||||
|
||||
for path in paths {
|
||||
if let Ok(file) = system_path_to_file(db, &path) {
|
||||
files.insert(file);
|
||||
}
|
||||
}
|
||||
|
||||
(files, diagnostics)
|
||||
let (files, diagnostics) = self.collect_vec(db);
|
||||
(files.into_iter().collect(), diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Self
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`Self` is treated as if it were a `TypeVar` bound to the class it's being used on.
|
||||
|
||||
`typing.Self` is only available in Python 3.11 and later.
|
||||
|
||||
## Methods
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
@@ -74,11 +74,6 @@ reveal_type(C().method()) # revealed: C
|
||||
|
||||
## Class Methods
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, TypeVar
|
||||
|
||||
@@ -101,11 +96,6 @@ reveal_type(Shape.bar()) # revealed: Unknown
|
||||
|
||||
## Attributes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
TODO: The use of `Self` to annotate the `next_node` attribute should be
|
||||
[modeled as a property][self attribute], using `Self` in its parameter and return type.
|
||||
|
||||
@@ -127,11 +117,6 @@ reveal_type(LinkedList().next()) # revealed: LinkedList
|
||||
|
||||
## Generic Classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, Generic, TypeVar
|
||||
|
||||
@@ -153,11 +138,6 @@ TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
|
||||
|
||||
## Annotations
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
@@ -171,11 +151,6 @@ class Shape:
|
||||
|
||||
`Self` cannot be used in the signature of a function or variable.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Self, Generic, TypeVar
|
||||
|
||||
@@ -218,4 +193,33 @@ class MyMetaclass(type):
|
||||
return super().__new__(cls)
|
||||
```
|
||||
|
||||
## Binding a method fixes `Self`
|
||||
|
||||
When a method is bound, any instances of `Self` in its signature are "fixed", since we now know the
|
||||
specific type of the bound parameter.
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class C:
|
||||
def instance_method(self, other: Self) -> Self:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def class_method(cls) -> Self:
|
||||
return cls()
|
||||
|
||||
# revealed: bound method C.instance_method(other: C) -> C
|
||||
reveal_type(C().instance_method)
|
||||
# revealed: bound method <class 'C'>.class_method() -> C
|
||||
reveal_type(C.class_method)
|
||||
|
||||
class D(C): ...
|
||||
|
||||
# revealed: bound method D.instance_method(other: D) -> D
|
||||
reveal_type(D().instance_method)
|
||||
# revealed: bound method <class 'D'>.class_method() -> D
|
||||
reveal_type(D.class_method)
|
||||
```
|
||||
|
||||
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations
|
||||
|
||||
@@ -198,7 +198,7 @@ python-version = "3.12"
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
|
||||
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any, /) -> _SpecialForm
|
||||
```
|
||||
|
||||
## Method calls on types not disjoint from `None`
|
||||
|
||||
@@ -876,8 +876,7 @@ def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any):
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(list_str)) # revealed: Unknown
|
||||
reveal_type(f(list_str)) # revealed: str
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(*(list_str,))) # revealed: Unknown
|
||||
|
||||
|
||||
@@ -331,6 +331,9 @@ instance or a subclass of the first. If either condition is violated, a `TypeErr
|
||||
runtime.
|
||||
|
||||
```py
|
||||
import typing
|
||||
import collections
|
||||
|
||||
def f(x: int):
|
||||
# error: [invalid-super-argument] "`int` is not a valid class"
|
||||
super(x, x)
|
||||
@@ -367,6 +370,19 @@ reveal_type(super(B, A))
|
||||
reveal_type(super(B, object))
|
||||
|
||||
super(object, object()).__class__
|
||||
|
||||
# Not all objects valid in a class's bases list are valid as the first argument to `super()`.
|
||||
# For example, it's valid to inherit from `typing.ChainMap`, but it's not valid as the first argument to `super()`.
|
||||
#
|
||||
# error: [invalid-super-argument] "`typing.ChainMap` is not a valid class"
|
||||
reveal_type(super(typing.ChainMap, collections.ChainMap())) # revealed: Unknown
|
||||
|
||||
# Meanwhile, it's not valid to inherit from unsubscripted `typing.Generic`,
|
||||
# but it *is* valid as the first argument to `super()`.
|
||||
reveal_type(super(typing.Generic, typing.SupportsInt)) # revealed: <super: typing.Generic, <class 'SupportsInt'>>
|
||||
|
||||
def _(x: type[typing.Any], y: typing.Any):
|
||||
reveal_type(super(x, y)) # revealed: <super: Any, Any>
|
||||
```
|
||||
|
||||
### Instance Member Access via `super`
|
||||
|
||||
@@ -988,6 +988,28 @@ class D: # error: [duplicate-kw-only]
|
||||
z: float
|
||||
```
|
||||
|
||||
`KW_ONLY` should only affect fields declared after it within the same class, not fields in
|
||||
subclasses:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass, KW_ONLY
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
_: KW_ONLY
|
||||
y: str
|
||||
|
||||
@dataclass
|
||||
class E(D):
|
||||
z: bytes
|
||||
|
||||
# This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only)
|
||||
E(1, b"foo", y="foo")
|
||||
|
||||
reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
|
||||
```
|
||||
|
||||
## Other special cases
|
||||
|
||||
### `dataclasses.dataclass`
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Invalid await diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Basic
|
||||
|
||||
This is a test showcasing a primitive case where an object is not awaitable.
|
||||
|
||||
```py
|
||||
async def main() -> None:
|
||||
await 1 # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Custom type with missing `__await__`
|
||||
|
||||
This diagnostic also points to the class definition if available.
|
||||
|
||||
```py
|
||||
class MissingAwait:
|
||||
pass
|
||||
|
||||
async def main() -> None:
|
||||
await MissingAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Custom type with possibly unbound `__await__`
|
||||
|
||||
This diagnostic also points to the method definition if available.
|
||||
|
||||
```py
|
||||
from datetime import datetime
|
||||
|
||||
class PossiblyUnbound:
|
||||
if datetime.today().weekday() == 0:
|
||||
def __await__(self):
|
||||
yield
|
||||
|
||||
async def main() -> None:
|
||||
await PossiblyUnbound() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## `__await__` definition with extra arguments
|
||||
|
||||
Currently, the signature of `__await__` isn't checked for conformity with the `Awaitable` protocol
|
||||
directly. Instead, individual anomalies are reported, such as the following. Here, the diagnostic
|
||||
reports that the object is not implicitly awaitable, while also pointing at the function parameters.
|
||||
|
||||
```py
|
||||
class InvalidAwaitArgs:
|
||||
def __await__(self, value: int):
|
||||
yield value
|
||||
|
||||
async def main() -> None:
|
||||
await InvalidAwaitArgs() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Non-callable `__await__`
|
||||
|
||||
This diagnostic doesn't point to the attribute definition, but complains about it being possibly not
|
||||
awaitable.
|
||||
|
||||
```py
|
||||
class NonCallableAwait:
|
||||
__await__ = 42
|
||||
|
||||
async def main() -> None:
|
||||
await NonCallableAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## `__await__` definition with explicit invalid return type
|
||||
|
||||
`__await__` must return a valid iterator. This diagnostic also points to the method definition if
|
||||
available.
|
||||
|
||||
```py
|
||||
class InvalidAwaitReturn:
|
||||
def __await__(self) -> int:
|
||||
return 5
|
||||
|
||||
async def main() -> None:
|
||||
await InvalidAwaitReturn() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Invalid union return type
|
||||
|
||||
When multiple potential definitions of `__await__` exist, all of them must be proper in order for an
|
||||
instance to be awaitable. In this specific case, no specific function definition is highlighted.
|
||||
|
||||
```py
|
||||
import typing
|
||||
from datetime import datetime
|
||||
|
||||
class UnawaitableUnion:
|
||||
if datetime.today().weekday() == 6:
|
||||
|
||||
def __await__(self) -> typing.Generator[typing.Any, None, None]:
|
||||
yield
|
||||
else:
|
||||
|
||||
def __await__(self) -> int:
|
||||
return 5
|
||||
|
||||
async def main() -> None:
|
||||
await UnawaitableUnion() # error: [invalid-await]
|
||||
```
|
||||
@@ -749,6 +749,51 @@ def singleton_check(value: Singleton) -> str:
|
||||
assert_never(value)
|
||||
```
|
||||
|
||||
## `__eq__` and `__ne__`
|
||||
|
||||
### No `__eq__` or `__ne__` overrides
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
|
||||
reveal_type(Color.RED == Color.RED) # revealed: Literal[True]
|
||||
reveal_type(Color.RED != Color.RED) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Overridden `__eq__`
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
reveal_type(Color.RED == Color.RED) # revealed: bool
|
||||
```
|
||||
|
||||
### Overridden `__ne__`
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
reveal_type(Color.RED != Color.RED) # revealed: bool
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>
|
||||
|
||||
@@ -491,6 +491,24 @@ class A(Generic[T]):
|
||||
reveal_type(A(x=1)) # revealed: A[int]
|
||||
```
|
||||
|
||||
### Class typevar has another typevar as a default
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U", default=T)
|
||||
|
||||
class C(Generic[T, U]): ...
|
||||
|
||||
reveal_type(C()) # revealed: C[Unknown, Unknown]
|
||||
|
||||
class D(Generic[T, U]):
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
reveal_type(D()) # revealed: D[Unknown, Unknown]
|
||||
```
|
||||
|
||||
## Generic subclass
|
||||
|
||||
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
|
||||
|
||||
@@ -455,3 +455,45 @@ def overloaded_outer(t: T | None = None) -> None:
|
||||
if t is not None:
|
||||
inner(t)
|
||||
```
|
||||
|
||||
## Unpacking a TypeVar
|
||||
|
||||
We can infer precise heterogeneous types from the result of an unpacking operation applied to a type
|
||||
variable if the type variable's upper bound is a type with a precise tuple spec:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple, Final, TypeVar, Generic
|
||||
|
||||
T = TypeVar("T", bound=tuple[int, str])
|
||||
|
||||
def f(x: T) -> T:
|
||||
a, b = x
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
return x
|
||||
|
||||
@dataclass
|
||||
class Team(Generic[T]):
|
||||
employees: list[T]
|
||||
|
||||
def x(team: Team[T]) -> Team[T]:
|
||||
age, name = team.employees[0]
|
||||
reveal_type(age) # revealed: int
|
||||
reveal_type(name) # revealed: str
|
||||
return team
|
||||
|
||||
class Age(int): ...
|
||||
class Name(str): ...
|
||||
|
||||
class Employee(NamedTuple):
|
||||
age: Age
|
||||
name: Name
|
||||
|
||||
EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),)
|
||||
team = Team(employees=list(EMPLOYEES))
|
||||
reveal_type(team.employees) # revealed: list[Employee]
|
||||
age, name = team.employees[0]
|
||||
reveal_type(age) # revealed: Age
|
||||
reveal_type(name) # revealed: Name
|
||||
```
|
||||
|
||||
@@ -237,10 +237,15 @@ If the type of a constructor parameter is a class typevar, we can use that to in
|
||||
parameter. The types inferred from a type context and from a constructor parameter must be
|
||||
consistent with each other.
|
||||
|
||||
We have to add `x: T` to the classes to ensure they're not bivariant in `T` (__new__ and __init__
|
||||
signatures don't count towards variance).
|
||||
|
||||
### `__new__` only
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
@@ -254,6 +259,8 @@ wrong_innards: C[int] = C("five")
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[int]
|
||||
@@ -266,6 +273,8 @@ wrong_innards: C[int] = C("five")
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
@@ -281,6 +290,8 @@ wrong_innards: C[int] = C("five")
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, *args, **kwargs) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
@@ -292,6 +303,8 @@ reveal_type(C(1)) # revealed: C[int]
|
||||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D[T]:
|
||||
x: T
|
||||
|
||||
def __new__(cls, x: T) -> "D[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
@@ -378,6 +391,8 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
def __init__[S](self, x: T, y: S) -> None: ...
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[int]
|
||||
@@ -395,6 +410,10 @@ from __future__ import annotations
|
||||
from typing import overload
|
||||
|
||||
class C[T]:
|
||||
# we need to use the type variable or else the class is bivariant in T, and
|
||||
# specializations become meaningless
|
||||
x: T
|
||||
|
||||
@overload
|
||||
def __init__(self: C[str], x: str) -> None: ...
|
||||
@overload
|
||||
@@ -438,6 +457,19 @@ class A[T]:
|
||||
reveal_type(A(x=1)) # revealed: A[int]
|
||||
```
|
||||
|
||||
### Class typevar has another typevar as a default
|
||||
|
||||
```py
|
||||
class C[T, U = T]: ...
|
||||
|
||||
reveal_type(C()) # revealed: C[Unknown, Unknown]
|
||||
|
||||
class D[T, U = T]:
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
reveal_type(D()) # revealed: D[Unknown, Unknown]
|
||||
```
|
||||
|
||||
## Generic subclass
|
||||
|
||||
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
|
||||
@@ -617,5 +649,34 @@ class C[T](C): ...
|
||||
class D[T](D[int]): ...
|
||||
```
|
||||
|
||||
## Tuple as a PEP-695 generic class
|
||||
|
||||
Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in
|
||||
typeshed:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class tuple[T]: ...
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
reveal_type((1, 2, 3)) # revealed: tuple[Literal[1], Literal[2], Literal[3]]
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
||||
|
||||
@@ -454,3 +454,43 @@ def overloaded_outer[T](t: T | None = None) -> None:
|
||||
if t is not None:
|
||||
inner(t)
|
||||
```
|
||||
|
||||
## Unpacking a TypeVar
|
||||
|
||||
We can infer precise heterogeneous types from the result of an unpacking operation applied to a
|
||||
TypeVar if the TypeVar's upper bound is a type with a precise tuple spec:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple, Final
|
||||
|
||||
def f[T: tuple[int, str]](x: T) -> T:
|
||||
a, b = x
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
return x
|
||||
|
||||
@dataclass
|
||||
class Team[T: tuple[int, str]]:
|
||||
employees: list[T]
|
||||
|
||||
def x[T: tuple[int, str]](team: Team[T]) -> Team[T]:
|
||||
age, name = team.employees[0]
|
||||
reveal_type(age) # revealed: int
|
||||
reveal_type(name) # revealed: str
|
||||
return team
|
||||
|
||||
class Age(int): ...
|
||||
class Name(str): ...
|
||||
|
||||
class Employee(NamedTuple):
|
||||
age: Age
|
||||
name: Name
|
||||
|
||||
EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),)
|
||||
team = Team(employees=list(EMPLOYEES))
|
||||
reveal_type(team.employees) # revealed: list[Employee]
|
||||
age, name = team.employees[0]
|
||||
reveal_type(age) # revealed: Age
|
||||
reveal_type(name) # revealed: Name
|
||||
```
|
||||
|
||||
@@ -40,8 +40,6 @@ class C[T]:
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
@@ -49,8 +47,6 @@ static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
static_assert(not is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
@@ -58,8 +54,6 @@ static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
@@ -67,8 +61,6 @@ static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
static_assert(not is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
@@ -124,8 +116,6 @@ class D[U](C[U]):
|
||||
pass
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
@@ -133,8 +123,6 @@ static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_assignable_to(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
@@ -142,8 +130,6 @@ static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
@@ -151,8 +137,6 @@ static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
@@ -297,34 +281,22 @@ class C[T]:
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(D[A], C[B]))
|
||||
static_assert(is_assignable_to(D[A], C[Any]))
|
||||
static_assert(is_assignable_to(D[B], C[Any]))
|
||||
static_assert(is_assignable_to(D[Any], C[A]))
|
||||
static_assert(is_assignable_to(D[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
@@ -332,11 +304,7 @@ static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
static_assert(not is_subtype_of(C[Any], C[Any]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(D[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[A], C[Any]))
|
||||
static_assert(not is_subtype_of(D[B], C[Any]))
|
||||
@@ -345,23 +313,11 @@ static_assert(not is_subtype_of(D[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[B], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[Any], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_equivalent_to(D[A], C[A]))
|
||||
@@ -380,4 +336,504 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Mutual Recursion
|
||||
|
||||
This example due to Martin Huschenbett's PyCon 2025 talk,
|
||||
[Linear Time variance Inference for PEP 695][linear-time-variance-talk]
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[X]:
|
||||
def f(self) -> "D[X]":
|
||||
return D()
|
||||
|
||||
def g(self, x: X) -> None: ...
|
||||
|
||||
class D[Y]:
|
||||
def h(self) -> C[Y]:
|
||||
return C()
|
||||
```
|
||||
|
||||
`C` is contravariant in `X`, and `D` in `Y`:
|
||||
|
||||
- `C` has two occurrences of `X`
|
||||
- `X` occurs in the return type of `f` as `D[X]` (`X` is substituted in for `Y`)
|
||||
- `D` has one occurrence of `Y`
|
||||
- `Y` occurs in the return type of `h` as `C[Y]`
|
||||
- `X` occurs contravariantly as a parameter in `g`
|
||||
|
||||
Thus the variance of `X` in `C` depends on itself. We want to infer the least restrictive possible
|
||||
variance, so in such cases we begin by assuming that the point where we detect the cycle is
|
||||
bivariant.
|
||||
|
||||
If we thus assume `X` is bivariant in `C`, then `Y` will be bivariant in `D`, as `D`'s only
|
||||
occurrence of `Y` is in `C`. Then we consider `X` in `C` once more. We have two occurrences: `D[X]`
|
||||
covariantly in a return type, and `X` contravariantly in an argument type. With one bivariant and
|
||||
one contravariant occurrence, we update our inference of `X` in `C` to contravariant---the supremum
|
||||
of contravariant and bivariant in the lattice.
|
||||
|
||||
Now that we've updated the variance of `X` in `C`, we re-evaluate `Y` in `D`. It only has the one
|
||||
occurrence `C[Y]`, which we now infer is contravariant, and so we infer contravariance for `Y` in
|
||||
`D` as well.
|
||||
|
||||
Because the variance of `X` in `C` depends on that of `Y` in `D`, we have to re-evaluate now that
|
||||
we've updated the latter to contravariant. The variance of `X` in `C` is now the supremum of
|
||||
contravariant and contravariant---giving us contravariant---and so remains unchanged.
|
||||
|
||||
Once we've completed a turn around the cycle with nothing changed, we've reached a fixed-point---the
|
||||
variance inference will not change any further---and so we finally conclude that both `X` in `C` and
|
||||
`Y` in `D` are contravariant.
|
||||
|
||||
```py
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(is_subtype_of(D[A], D[B]))
|
||||
static_assert(not is_subtype_of(D[A], D[Any]))
|
||||
static_assert(not is_subtype_of(D[B], D[Any]))
|
||||
static_assert(not is_subtype_of(D[Any], D[A]))
|
||||
static_assert(not is_subtype_of(D[Any], D[B]))
|
||||
```
|
||||
|
||||
## Class Attributes
|
||||
|
||||
### Mutable Attributes
|
||||
|
||||
Normal attributes are mutable, and so make the enclosing class invariant in this typevar (see
|
||||
[inv]).
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
One might think that occurrences in the types of normal attributes are covariant, but they are
|
||||
mutable, and thus the occurrences are invariant.
|
||||
|
||||
### Immutable Attributes
|
||||
|
||||
Immutable attributes can't be written to, and thus constrain the typevar to covariance, not
|
||||
invariance.
|
||||
|
||||
#### Final attributes
|
||||
|
||||
```py
|
||||
from typing import Final
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
x: Final[T]
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
#### Underscore-prefixed attributes
|
||||
|
||||
Underscore-prefixed instance attributes are considered private, and thus are assumed not externally
|
||||
mutated.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
_x: T
|
||||
|
||||
@property
|
||||
def x(self) -> T:
|
||||
return self._x
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
|
||||
class D[T]:
|
||||
def __init__(self, x: T):
|
||||
self._x = x
|
||||
|
||||
@property
|
||||
def x(self) -> T:
|
||||
return self._x
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
#### Frozen dataclasses in Python 3.12 and earlier
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass, field
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class D[U]:
|
||||
y: U
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class E[U]:
|
||||
y: U = field()
|
||||
|
||||
static_assert(is_subtype_of(E[B], E[A]))
|
||||
static_assert(not is_subtype_of(E[A], E[B]))
|
||||
```
|
||||
|
||||
#### Frozen dataclasses in Python 3.13 and later
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
Python 3.13 introduced a new synthesized `__replace__` method on dataclasses, which uses every field
|
||||
type in a contravariant position (as a parameter to `__replace__`). This means that frozen
|
||||
dataclasses on Python 3.13+ can't be covariant in their field types.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class D[U]:
|
||||
y: U
|
||||
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
#### NamedTuple
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class E[V](NamedTuple):
|
||||
z: V
|
||||
|
||||
static_assert(is_subtype_of(E[B], E[A]))
|
||||
static_assert(not is_subtype_of(E[A], E[B]))
|
||||
```
|
||||
|
||||
A subclass of a `NamedTuple` can still be covariant:
|
||||
|
||||
```py
|
||||
class D[T](E[T]):
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
But adding a new generic attribute on the subclass makes it invariant (the added attribute is not a
|
||||
`NamedTuple` field, and thus not immutable):
|
||||
|
||||
```py
|
||||
class C[T](E[T]):
|
||||
w: T
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
Properties constrain to covariance if they are get-only and invariant if they are get-set:
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
@property
|
||||
def x(self) -> T | None:
|
||||
return None
|
||||
|
||||
class D[U]:
|
||||
@property
|
||||
def y(self) -> U | None:
|
||||
return None
|
||||
|
||||
@y.setter
|
||||
def y(self, value: U): ...
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
### Implicit Attributes
|
||||
|
||||
Implicit attributes work like normal ones
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def f(self) -> None:
|
||||
self.x: T | None = None
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
### Constructors: excluding `__init__` and `__new__`
|
||||
|
||||
We consider it invalid to call `__init__` explicitly on an existing object. Likewise, `__new__` is
|
||||
only used at the beginning of an object's life. As such, we don't need to worry about the variance
|
||||
impact of these methods.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_subtype_of
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def __init__(self, x: T): ...
|
||||
def __new__(self, x: T): ...
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
```
|
||||
|
||||
This example is then bivariant because it doesn't use `T` outside of the two exempted methods.
|
||||
|
||||
This holds likewise for dataclasses with synthesized `__init__`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=True, frozen=True)
|
||||
class D[T]:
|
||||
x: T
|
||||
|
||||
# Covariant due to the read-only T-typed attribute; the `__init__` is ignored and doesn't make it
|
||||
# invariant:
|
||||
|
||||
static_assert(is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
## Union Types
|
||||
|
||||
Union types are covariant in all their members. If `A <: B`, then `A | C <: B | C` and
|
||||
`C | A <: C | B`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C: ...
|
||||
|
||||
# Union types are covariant in their members
|
||||
static_assert(is_subtype_of(B | C, A | C))
|
||||
static_assert(is_subtype_of(C | B, C | A))
|
||||
static_assert(not is_subtype_of(A | C, B | C))
|
||||
static_assert(not is_subtype_of(C | A, C | B))
|
||||
|
||||
# Assignability follows the same pattern
|
||||
static_assert(is_assignable_to(B | C, A | C))
|
||||
static_assert(is_assignable_to(C | B, C | A))
|
||||
static_assert(not is_assignable_to(A | C, B | C))
|
||||
static_assert(not is_assignable_to(C | A, C | B))
|
||||
```
|
||||
|
||||
## Intersection Types
|
||||
|
||||
Intersection types cannot be expressed directly in Python syntax, but they occur when type narrowing
|
||||
creates constraints through control flow. In ty's representation, intersection types are covariant
|
||||
in their positive conjuncts and contravariant in their negative conjuncts.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
class C: ...
|
||||
|
||||
# Test covariance in positive conjuncts
|
||||
# If B <: A, then Intersection[X, B] <: Intersection[X, A]
|
||||
static_assert(is_subtype_of(Intersection[C, B], Intersection[C, A]))
|
||||
static_assert(not is_subtype_of(Intersection[C, A], Intersection[C, B]))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[C, B], Intersection[C, A]))
|
||||
static_assert(not is_assignable_to(Intersection[C, A], Intersection[C, B]))
|
||||
|
||||
# Test contravariance in negative conjuncts
|
||||
# If B <: A, then Intersection[X, Not[A]] <: Intersection[X, Not[B]]
|
||||
# (excluding supertype A is more restrictive than excluding subtype B)
|
||||
static_assert(is_subtype_of(Intersection[C, Not[A]], Intersection[C, Not[B]]))
|
||||
static_assert(not is_subtype_of(Intersection[C, Not[B]], Intersection[C, Not[A]]))
|
||||
|
||||
static_assert(is_assignable_to(Intersection[C, Not[A]], Intersection[C, Not[B]]))
|
||||
static_assert(not is_assignable_to(Intersection[C, Not[B]], Intersection[C, Not[A]]))
|
||||
```
|
||||
|
||||
## Subclass Types (type[T])
|
||||
|
||||
The `type[T]` construct represents the type of classes that are subclasses of `T`. It is covariant
|
||||
in `T` because if `A <: B`, then `type[A] <: type[B]` holds.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
# type[T] is covariant in T
|
||||
static_assert(is_subtype_of(type[B], type[A]))
|
||||
static_assert(not is_subtype_of(type[A], type[B]))
|
||||
|
||||
static_assert(is_assignable_to(type[B], type[A]))
|
||||
static_assert(not is_assignable_to(type[A], type[B]))
|
||||
|
||||
# With generic classes using type[T]
|
||||
class ClassContainer[T]:
|
||||
def __init__(self, cls: type[T]) -> None:
|
||||
self.cls = cls
|
||||
|
||||
def create_instance(self) -> T:
|
||||
return self.cls()
|
||||
|
||||
# ClassContainer is covariant in T due to type[T]
|
||||
static_assert(is_subtype_of(ClassContainer[B], ClassContainer[A]))
|
||||
static_assert(not is_subtype_of(ClassContainer[A], ClassContainer[B]))
|
||||
|
||||
static_assert(is_assignable_to(ClassContainer[B], ClassContainer[A]))
|
||||
static_assert(not is_assignable_to(ClassContainer[A], ClassContainer[B]))
|
||||
|
||||
# Practical example: you can pass a ClassContainer[B] where ClassContainer[A] is expected
|
||||
# because type[B] can safely be used where type[A] is expected
|
||||
def use_a_class_container(container: ClassContainer[A]) -> A:
|
||||
return container.create_instance()
|
||||
|
||||
b_container = ClassContainer[B](B)
|
||||
a_instance: A = use_a_class_container(b_container) # This should work
|
||||
```
|
||||
|
||||
## Inheriting from generic classes with inferred variance
|
||||
|
||||
When inheriting from a generic class with our type variable substituted in, we count its occurrences
|
||||
as well. In the following example, `T` is covariant in `C`, and contravariant in the subclass `D` if
|
||||
you only count its own occurrences. Because we count both then, `T` is invariant in `D`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class C[T]:
|
||||
def f() -> T | None:
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
|
||||
class D[T](C[T]):
|
||||
def g(x: T) -> None:
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(D[B], D[A]))
|
||||
static_assert(not is_subtype_of(D[A], D[B]))
|
||||
```
|
||||
|
||||
## Inheriting from generic classes with explicit variance
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
T = TypeVar("T")
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
T_contra = TypeVar("T_contra", contravariant=True)
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class Invariant(Generic[T]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(Invariant[B], Invariant[A]))
|
||||
static_assert(not is_subtype_of(Invariant[A], Invariant[B]))
|
||||
|
||||
class DerivedInvariant[T](Invariant[T]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(DerivedInvariant[B], DerivedInvariant[A]))
|
||||
static_assert(not is_subtype_of(DerivedInvariant[A], DerivedInvariant[B]))
|
||||
|
||||
class Covariant(Generic[T_co]):
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(Covariant[B], Covariant[A]))
|
||||
static_assert(not is_subtype_of(Covariant[A], Covariant[B]))
|
||||
|
||||
class DerivedCovariant[T](Covariant[T]):
|
||||
pass
|
||||
|
||||
static_assert(is_subtype_of(DerivedCovariant[B], DerivedCovariant[A]))
|
||||
static_assert(not is_subtype_of(DerivedCovariant[A], DerivedCovariant[B]))
|
||||
|
||||
class Contravariant(Generic[T_contra]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(Contravariant[B], Contravariant[A]))
|
||||
static_assert(is_subtype_of(Contravariant[A], Contravariant[B]))
|
||||
|
||||
class DerivedContravariant[T](Contravariant[T]):
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(DerivedContravariant[B], DerivedContravariant[A]))
|
||||
static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B]))
|
||||
```
|
||||
|
||||
[linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
# Partial stub packages
|
||||
|
||||
Partial stub packages are stubs that are allowed to be missing modules. See the
|
||||
[specification](https://peps.python.org/pep-0561/#partial-stub-packages). Partial stubs are also
|
||||
called "incomplete" packages, and non-partial stubs are called "complete" packages.
|
||||
|
||||
Normally a stub package is expected to define a copy of every module the real implementation
|
||||
defines. Module resolution is consequently required to report a module doesn't exist if it finds
|
||||
`mypackage-stubs` and fails to find `mypackage.mymodule` *even if* `mypackage` does define
|
||||
`mymodule`.
|
||||
|
||||
If a stub package declares that it's partial, we instead are expected to fall through to the
|
||||
implementation package and try to discover `mymodule` there. This is described as follows:
|
||||
|
||||
> Type checkers should merge the stub package and runtime package or typeshed directories. This can
|
||||
> be thought of as the functional equivalent of copying the stub package into the same directory as
|
||||
> the corresponding runtime package or typeshed folder and type checking the combined directory
|
||||
> structure. Thus type checkers MUST maintain the normal resolution order of checking `*.pyi` before
|
||||
> `*.py` files.
|
||||
|
||||
Namespace stub packages are always considered partial by necessity. Regular stub packages are only
|
||||
considered partial if they define a `py.typed` file containing the string `partial\n` (due to real
|
||||
stubs in the wild, we relax this and look case-insensitively for `partial`).
|
||||
|
||||
The `py.typed` file was originally specified as an empty marker for "this package supports types",
|
||||
as a way to opt into having typecheckers run on a package. However ty and pyright choose to largely
|
||||
ignore this and just type check every package.
|
||||
|
||||
In its original specification it was specified that subpackages inherit any `py.typed` declared in a
|
||||
parent package. However the precise interaction with `partial\n` was never specified. We currently
|
||||
implement a simple inheritance scheme where a subpackage can always declare its own `py.typed` and
|
||||
override whether it's partial or not.
|
||||
|
||||
## Partial stub with missing module
|
||||
|
||||
A stub package that includes a partial `py.typed` file.
|
||||
|
||||
Here "both" is found in the stub, while "impl" is found in the implementation. "fake" is found in
|
||||
neither.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/py.typed`:
|
||||
|
||||
```text
|
||||
partial
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/foo/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo.both import Both
|
||||
from foo.impl import Impl
|
||||
from foo.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: str
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Non-partial stub with missing module
|
||||
|
||||
Omitting the partial `py.typed`, we see "impl" now cannot be found.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/foo/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo.both import Both
|
||||
from foo.impl import Impl # error: "Cannot resolve"
|
||||
from foo.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: Unknown
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Full-typed stub with missing module
|
||||
|
||||
Including a blank py.typed we still don't conclude it's partial.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/py.typed`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/foo/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo.both import Both
|
||||
from foo.impl import Impl # error: "Cannot resolve"
|
||||
from foo.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: Unknown
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inheriting a partial `py.typed`
|
||||
|
||||
`foo-stubs` defines a partial py.typed which is used by `foo-stubs/bar`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/py.typed`:
|
||||
|
||||
```text
|
||||
# Also testing permissive parsing
|
||||
# PARTIAL\n
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/bar/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/bar/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/foo/bar/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo.bar.both import Both
|
||||
from foo.bar.impl import Impl
|
||||
from foo.bar.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: str
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Overloading a full `py.typed`
|
||||
|
||||
`foo-stubs` defines a full py.typed which is overloaded to partial by `foo-stubs/bar`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/py.typed`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/py.typed`:
|
||||
|
||||
```text
|
||||
# Also testing permissive parsing
|
||||
partial/n
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/bar/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/bar/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/foo/bar/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo.bar.both import Both
|
||||
from foo.bar.impl import Impl
|
||||
from foo.bar.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: str
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Overloading a partial `py.typed`
|
||||
|
||||
`foo-stubs` defines a partial py.typed which is overloaded to full by `foo-stubs/bar`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/py.typed`:
|
||||
|
||||
```text
|
||||
# Also testing permissive parsing
|
||||
pArTiAl\n
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/py.typed`:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/bar/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/bar/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/foo/bar/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/foo/bar/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo.bar.both import Both
|
||||
from foo.bar.impl import Impl # error: "Cannot resolve"
|
||||
from foo.bar.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: Unknown
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Namespace stub with missing module
|
||||
|
||||
Namespace stubs are always partial.
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/520>.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/parent-stubs/foo/both.pyi`:
|
||||
|
||||
```pyi
|
||||
class Both:
|
||||
both: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`/packages/parent/foo/both.py`:
|
||||
|
||||
```py
|
||||
class Both: ...
|
||||
```
|
||||
|
||||
`/packages/parent/foo/impl.py`:
|
||||
|
||||
```py
|
||||
class Impl:
|
||||
impl: str
|
||||
other: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from parent.foo.both import Both
|
||||
from parent.foo.impl import Impl
|
||||
from parent.foo.fake import Fake # error: "Cannot resolve"
|
||||
|
||||
reveal_type(Both().both) # revealed: str
|
||||
reveal_type(Impl().impl) # revealed: str
|
||||
reveal_type(Fake().fake) # revealed: Unknown
|
||||
```
|
||||
@@ -74,8 +74,16 @@ Person(3, "Eve", 99, "extra")
|
||||
# error: [invalid-argument-type]
|
||||
Person(id="3", name="Eve")
|
||||
|
||||
# TODO: over-writing NamedTuple fields should be an error
|
||||
reveal_type(Person.id) # revealed: property
|
||||
reveal_type(Person.name) # revealed: property
|
||||
reveal_type(Person.age) # revealed: property
|
||||
|
||||
# TODO... the error is correct, but this is not the friendliest error message
|
||||
# for assigning to a read-only property :-)
|
||||
#
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `id` on type `Person` with custom `__set__` method"
|
||||
alice.id = 42
|
||||
# error: [invalid-assignment]
|
||||
bob.age = None
|
||||
```
|
||||
|
||||
@@ -94,28 +102,59 @@ reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax)
|
||||
|
||||
### Definition
|
||||
|
||||
TODO: Fields without default values should come before fields with.
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Fields without default values must come before fields with.
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class Location(NamedTuple):
|
||||
altitude: float = 0.0
|
||||
latitude: float # this should be an error
|
||||
# error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitude` defined here without a default value"
|
||||
latitude: float
|
||||
# error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitude` defined here without a default value"
|
||||
longitude: float
|
||||
|
||||
class StrangeLocation(NamedTuple):
|
||||
altitude: float
|
||||
altitude: float = 0.0
|
||||
altitude: float
|
||||
altitude: float = 0.0
|
||||
latitude: float # error: [invalid-named-tuple]
|
||||
longitude: float # error: [invalid-named-tuple]
|
||||
|
||||
class VeryStrangeLocation(NamedTuple):
|
||||
altitude: float = 0.0
|
||||
latitude: float # error: [invalid-named-tuple]
|
||||
longitude: float # error: [invalid-named-tuple]
|
||||
altitude: float = 0.0
|
||||
```
|
||||
|
||||
### Multiple Inheritance
|
||||
|
||||
Multiple inheritance is not supported for `NamedTuple` classes:
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Multiple inheritance is not supported for `NamedTuple` classes except with `Generic`:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, Protocol
|
||||
|
||||
# This should ideally emit a diagnostic
|
||||
# error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
|
||||
class C(NamedTuple, object):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
# fmt: off
|
||||
|
||||
class D(
|
||||
int, # error: [invalid-named-tuple]
|
||||
NamedTuple
|
||||
): ...
|
||||
|
||||
# fmt: on
|
||||
|
||||
# error: [invalid-named-tuple]
|
||||
class E(NamedTuple, Protocol): ...
|
||||
```
|
||||
|
||||
### Inheriting from a `NamedTuple`
|
||||
@@ -151,9 +190,42 @@ from typing import NamedTuple
|
||||
class User(NamedTuple):
|
||||
id: int
|
||||
name: str
|
||||
age: int | None
|
||||
nickname: str
|
||||
|
||||
class SuperUser(User):
|
||||
id: int # this should be an error
|
||||
# TODO: this should be an error because it implies that the `id` attribute on
|
||||
# `SuperUser` is mutable, but the read-only `id` property from the superclass
|
||||
# has not been overridden in the class body
|
||||
id: int
|
||||
|
||||
# this is fine; overriding a read-only attribute with a mutable one
|
||||
# does not conflict with the Liskov Substitution Principle
|
||||
name: str = "foo"
|
||||
|
||||
# this is also fine
|
||||
@property
|
||||
def age(self) -> int:
|
||||
return super().age or 42
|
||||
|
||||
def now_called_robert(self):
|
||||
self.name = "Robert" # fine because overridden with a mutable attribute
|
||||
|
||||
# TODO: this should cause us to emit an error as we're assigning to a read-only property
|
||||
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
|
||||
self.nickname = "Bob"
|
||||
|
||||
james = SuperUser(0, "James", 42, "Jimmy")
|
||||
|
||||
# fine because the property on the superclass was overridden with a mutable attribute
|
||||
# on the subclass
|
||||
james.name = "Robert"
|
||||
|
||||
# TODO: the error is correct (can't assign to the read-only property inherited from the superclass)
|
||||
# but the error message could be friendlier :-)
|
||||
#
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `nickname` on type `SuperUser` with custom `__set__` method"
|
||||
james.nickname = "Bob"
|
||||
```
|
||||
|
||||
### Generic named tuples
|
||||
@@ -164,13 +236,29 @@ python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, Generic, TypeVar
|
||||
|
||||
class Property[T](NamedTuple):
|
||||
name: str
|
||||
value: T
|
||||
|
||||
reveal_type(Property("height", 3.4)) # revealed: Property[float]
|
||||
reveal_type(Property.value) # revealed: property
|
||||
reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown
|
||||
reveal_type(Property[str].value.fget) # revealed: (self, /) -> str
|
||||
reveal_type(Property("height", 3.4).value) # revealed: float
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class LegacyProperty(NamedTuple, Generic[T]):
|
||||
name: str
|
||||
value: T
|
||||
|
||||
reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int]
|
||||
reveal_type(LegacyProperty.value) # revealed: property
|
||||
reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown
|
||||
reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
|
||||
reveal_type(LegacyProperty("height", 3.4).value) # revealed: float
|
||||
```
|
||||
|
||||
## Attributes on `NamedTuple`
|
||||
@@ -186,7 +274,7 @@ class Person(NamedTuple):
|
||||
|
||||
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
|
||||
reveal_type(Person._fields) # revealed: tuple[str, ...]
|
||||
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@_make
|
||||
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
|
||||
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
|
||||
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
|
||||
|
||||
@@ -211,6 +299,91 @@ alice = Person(1, "Alice", 42)
|
||||
bob = Person(2, "Bob")
|
||||
```
|
||||
|
||||
## The symbol `NamedTuple` itself
|
||||
|
||||
At runtime, `NamedTuple` is a function, and we understand this:
|
||||
|
||||
```py
|
||||
import types
|
||||
import typing
|
||||
|
||||
def expects_functiontype(x: types.FunctionType): ...
|
||||
|
||||
expects_functiontype(typing.NamedTuple)
|
||||
```
|
||||
|
||||
This means we also understand that all attributes on function objects are available on the symbol
|
||||
`typing.NamedTuple`:
|
||||
|
||||
```py
|
||||
reveal_type(typing.NamedTuple.__name__) # revealed: str
|
||||
reveal_type(typing.NamedTuple.__qualname__) # revealed: str
|
||||
reveal_type(typing.NamedTuple.__kwdefaults__) # revealed: dict[str, Any] | None
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic and reveal `Unknown` (function objects don't have an `__mro__` attribute),
|
||||
# but the fact that we don't isn't actually a `NamedTuple` bug (https://github.com/astral-sh/ty/issues/986)
|
||||
reveal_type(typing.NamedTuple.__mro__) # revealed: tuple[<class 'FunctionType'>, <class 'object'>]
|
||||
```
|
||||
|
||||
By the normal rules, `NamedTuple` and `type[NamedTuple]` should not be valid in type expressions --
|
||||
there is no object at runtime that is an "instance of `NamedTuple`", nor is there any class at
|
||||
runtime that is a "subclass of `NamedTuple`" -- these are both impossible, since `NamedTuple` is a
|
||||
function and not a class. However, for compatibility with other type checkers, we allow `NamedTuple`
|
||||
in type expressions and understand it as describing an interface that all `NamedTuple` classes would
|
||||
satisfy:
|
||||
|
||||
```py
|
||||
def expects_named_tuple(x: typing.NamedTuple):
|
||||
reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike
|
||||
reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike
|
||||
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(**kwargs) -> NamedTupleLike
|
||||
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]]
|
||||
reveal_type(x.__add__)
|
||||
reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object]
|
||||
|
||||
def _(y: type[typing.NamedTuple]):
|
||||
reveal_type(y) # revealed: @Todo(unsupported type[X] special form)
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.NamedTuple` expected no type parameter"
|
||||
def _(z: typing.NamedTuple[int]): ...
|
||||
```
|
||||
|
||||
Any instance of a `NamedTuple` class can therefore be passed for a function parameter that is
|
||||
annotated with `NamedTuple`:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Protocol, Iterable, Any
|
||||
from ty_extensions import static_assert, is_assignable_to
|
||||
|
||||
class Point(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
reveal_type(Point._make) # revealed: bound method <class 'Point'>._make(iterable: Iterable[Any]) -> Point
|
||||
reveal_type(Point._asdict) # revealed: def _asdict(self) -> dict[str, Any]
|
||||
reveal_type(Point._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
|
||||
|
||||
static_assert(is_assignable_to(Point, NamedTuple))
|
||||
|
||||
expects_named_tuple(Point(x=42, y=56)) # fine
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `expects_named_tuple` is incorrect: Expected `tuple[object, ...] & NamedTupleLike`, found `tuple[Literal[1], Literal[2]]`"
|
||||
expects_named_tuple((1, 2))
|
||||
```
|
||||
|
||||
The type described by `NamedTuple` in type expressions is understood as being assignable to
|
||||
`tuple[object, ...]` and `tuple[Any, ...]`:
|
||||
|
||||
```py
|
||||
static_assert(is_assignable_to(NamedTuple, tuple))
|
||||
static_assert(is_assignable_to(NamedTuple, tuple[object, ...]))
|
||||
static_assert(is_assignable_to(NamedTuple, tuple[Any, ...]))
|
||||
|
||||
def expects_tuple(x: tuple[object, ...]): ...
|
||||
def _(x: NamedTuple):
|
||||
expects_tuple(x) # fine
|
||||
```
|
||||
|
||||
## NamedTuple with custom `__getattr__`
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/322>. Make sure that the
|
||||
|
||||
@@ -240,6 +240,21 @@ def f(x: str | None):
|
||||
|
||||
# When there is a reassignment, any narrowing constraints on the place are invalidated in lazy scopes.
|
||||
x = None
|
||||
|
||||
def f(x: str | None):
|
||||
def _():
|
||||
if x is not None:
|
||||
def closure():
|
||||
reveal_type(x) # revealed: str | None
|
||||
x = None
|
||||
|
||||
def f(x: str | None):
|
||||
class C:
|
||||
def _():
|
||||
if x is not None:
|
||||
def closure():
|
||||
reveal_type(x) # revealed: str
|
||||
x = None # This assignment is not visible in the inner lazy scope, so narrowing is still valid.
|
||||
```
|
||||
|
||||
If a variable defined in a private scope is never reassigned, narrowing remains in effect in the
|
||||
@@ -256,6 +271,12 @@ def f(const: str | None):
|
||||
reveal_type(const) # revealed: str
|
||||
|
||||
[reveal_type(const) for _ in range(1)] # revealed: str
|
||||
|
||||
def f(const: str | None):
|
||||
def _():
|
||||
if const is not None:
|
||||
def closure():
|
||||
reveal_type(const) # revealed: str
|
||||
```
|
||||
|
||||
And even if there is an attribute or subscript assignment to the variable, narrowing of the variable
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
## `type(x) is C`
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B):
|
||||
@final
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B, y: A | C):
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
@@ -14,20 +19,105 @@ def _(x: A | B):
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(y) is C:
|
||||
reveal_type(y) # revealed: C
|
||||
else:
|
||||
# here, however, inferring `A` is fine,
|
||||
# because `C` is `@final`: no subclass of `A`
|
||||
# and `C` could exist
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
if type(y) is A:
|
||||
reveal_type(y) # revealed: A
|
||||
else:
|
||||
# but here, `type(y)` could be a subclass of `A`,
|
||||
# in which case the `type(y) is A` call would evaluate
|
||||
# to `False` even if `y` was an instance of `A`,
|
||||
# so narrowing cannot occur
|
||||
reveal_type(y) # revealed: A | C
|
||||
```
|
||||
|
||||
## `type(x) is not C`
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B):
|
||||
@final
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B, y: A | C):
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
|
||||
if type(y) is not C:
|
||||
# same reasoning as above: narrowing *can* occur here because `C` is `@final`
|
||||
reveal_type(y) # revealed: A
|
||||
else:
|
||||
reveal_type(y) # revealed: C
|
||||
|
||||
if type(y) is not A:
|
||||
# same reasoning as above: narrowing *cannot* occur here
|
||||
# because `A` is not `@final`
|
||||
reveal_type(y) # revealed: A | C
|
||||
else:
|
||||
reveal_type(y) # revealed: A
|
||||
```
|
||||
|
||||
## No narrowing for `type(x) is C[int]`
|
||||
|
||||
At runtime, `type(x)` will never return a generic alias object (only ever a class-literal object),
|
||||
so no narrowing can occur if `type(x)` is compared with a generic alias object.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class A[T]: ...
|
||||
class B: ...
|
||||
|
||||
def f(x: A[int] | B):
|
||||
if type(x) is A[int]:
|
||||
# this branch is actually unreachable -- we *could* reveal `Never` here!
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
|
||||
if type(x) is A:
|
||||
# TODO: this should be `A[int]`, but `A[int] | B` would be better than `Never`
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
|
||||
if type(x) is B:
|
||||
reveal_type(x) # revealed: B
|
||||
else:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
|
||||
if type(x) is not A[int]:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
else:
|
||||
# this branch is actually unreachable -- we *could* reveal `Never` here!
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
|
||||
if type(x) is not A:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
else:
|
||||
# TODO: this should be `A[int]`, but `A[int] | B` would be better than `Never`
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if type(x) is not B:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
else:
|
||||
reveal_type(x) # revealed: B
|
||||
```
|
||||
|
||||
## `type(x) == C`, `type(x) != C`
|
||||
@@ -127,12 +217,23 @@ class B: ...
|
||||
|
||||
def _[T](x: A | B):
|
||||
if type(x) is A[str]:
|
||||
# `type()` never returns a generic alias, so `type(x)` cannot be `A[str]`
|
||||
reveal_type(x) # revealed: Never
|
||||
# TODO: `type()` never returns a generic alias, so `type(x)` cannot be `A[str]`
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
```
|
||||
|
||||
## Narrowing for tuple
|
||||
|
||||
An early version of <https://github.com/astral-sh/ruff/pull/19920> caused us to crash on this:
|
||||
|
||||
```py
|
||||
def _(val):
|
||||
if type(val) is tuple:
|
||||
# TODO: better would be `Unknown & tuple[object, ...]`
|
||||
reveal_type(val) # revealed: Unknown & tuple[Unknown, ...]
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
```py
|
||||
|
||||
@@ -64,6 +64,17 @@ x: MyIntOrStr = 1
|
||||
y: MyIntOrStr = None
|
||||
```
|
||||
|
||||
## Unpacking from a type alias
|
||||
|
||||
```py
|
||||
type T = tuple[int, str]
|
||||
|
||||
def f(x: T):
|
||||
a, b = x
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
```
|
||||
|
||||
## Generic type aliases
|
||||
|
||||
```py
|
||||
|
||||
@@ -150,6 +150,14 @@ class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ...
|
||||
|
||||
# revealed: tuple[<class 'AlsoInvalid'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, <class 'NotAProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
reveal_type(AlsoInvalid.__mro__)
|
||||
|
||||
class NotAGenericProtocol[T]: ...
|
||||
|
||||
# error: [invalid-protocol] "Protocol class `StillInvalid` cannot inherit from non-protocol class `NotAGenericProtocol`"
|
||||
class StillInvalid(NotAGenericProtocol[int], Protocol): ...
|
||||
|
||||
# revealed: tuple[<class 'StillInvalid'>, <class 'NotAGenericProtocol[int]'>, typing.Protocol, typing.Generic, <class 'object'>]
|
||||
reveal_type(StillInvalid.__mro__)
|
||||
```
|
||||
|
||||
But two exceptions to this rule are `object` and `Generic`:
|
||||
@@ -347,7 +355,9 @@ And as a corollary, `type[MyProtocol]` can also be called:
|
||||
|
||||
```py
|
||||
def f(x: type[MyProtocol]):
|
||||
reveal_type(x()) # revealed: MyProtocol
|
||||
# TODO: add a `reveal_type` call here once it's no longer a `Todo` type
|
||||
# (which doesn't work well with snapshots)
|
||||
x()
|
||||
```
|
||||
|
||||
## Members of a protocol
|
||||
@@ -387,7 +397,7 @@ To see the kinds and types of the protocol members, you can use the debugging ai
|
||||
|
||||
```py
|
||||
from ty_extensions import reveal_protocol_interface
|
||||
from typing import SupportsIndex, SupportsAbs
|
||||
from typing import SupportsIndex, SupportsAbs, ClassVar
|
||||
|
||||
# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`"
|
||||
reveal_protocol_interface(Foo)
|
||||
@@ -405,6 +415,33 @@ reveal_protocol_interface("foo")
|
||||
#
|
||||
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
|
||||
reveal_protocol_interface(SupportsAbs[int])
|
||||
|
||||
class BaseProto(Protocol):
|
||||
def member(self) -> int: ...
|
||||
|
||||
class SubProto(BaseProto, Protocol):
|
||||
def member(self) -> bool: ...
|
||||
|
||||
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> int`)}`"
|
||||
reveal_protocol_interface(BaseProto)
|
||||
|
||||
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> bool`)}`"
|
||||
reveal_protocol_interface(SubProto)
|
||||
|
||||
class ProtoWithClassVar(Protocol):
|
||||
x: ClassVar[int]
|
||||
|
||||
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
|
||||
reveal_protocol_interface(ProtoWithClassVar)
|
||||
|
||||
class ProtocolWithDefault(Protocol):
|
||||
x: int = 0
|
||||
|
||||
# We used to incorrectly report this as having an `x: Literal[0]` member;
|
||||
# declared types should take priority over inferred types for protocol interfaces!
|
||||
#
|
||||
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`)}`"
|
||||
reveal_protocol_interface(ProtocolWithDefault)
|
||||
```
|
||||
|
||||
Certain special attributes and methods are not considered protocol members at runtime, and should
|
||||
@@ -445,6 +482,8 @@ reveal_type(get_protocol_members(Baz2))
|
||||
|
||||
## Protocol members in statically known branches
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
The list of protocol members does not include any members declared in branches that are statically
|
||||
known to be unreachable:
|
||||
|
||||
@@ -455,7 +494,7 @@ python-version = "3.9"
|
||||
|
||||
```py
|
||||
import sys
|
||||
from typing_extensions import Protocol, get_protocol_members
|
||||
from typing_extensions import Protocol, get_protocol_members, reveal_type
|
||||
|
||||
class Foo(Protocol):
|
||||
if sys.version_info >= (3, 10):
|
||||
@@ -464,7 +503,7 @@ class Foo(Protocol):
|
||||
def c(self) -> None: ...
|
||||
else:
|
||||
d: int
|
||||
e = 56
|
||||
e = 56 # error: [ambiguous-protocol-member]
|
||||
def f(self) -> None: ...
|
||||
|
||||
reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]]
|
||||
@@ -613,13 +652,26 @@ class HasXWithDefault(Protocol):
|
||||
class FooWithZero:
|
||||
x: int = 0
|
||||
|
||||
# TODO: these should pass
|
||||
static_assert(is_subtype_of(FooWithZero, HasXWithDefault)) # error: [static-assert-error]
|
||||
static_assert(is_assignable_to(FooWithZero, HasXWithDefault)) # error: [static-assert-error]
|
||||
static_assert(not is_subtype_of(Foo, HasXWithDefault))
|
||||
static_assert(not is_assignable_to(Foo, HasXWithDefault))
|
||||
static_assert(not is_subtype_of(Qux, HasXWithDefault))
|
||||
static_assert(not is_assignable_to(Qux, HasXWithDefault))
|
||||
static_assert(is_subtype_of(FooWithZero, HasXWithDefault))
|
||||
static_assert(is_assignable_to(FooWithZero, HasXWithDefault))
|
||||
|
||||
# TODO: whether or not any of these four assertions should pass is not clearly specified.
|
||||
#
|
||||
# A test in the typing conformance suite implies that they all should:
|
||||
# that a nominal class with an instance attribute `x`
|
||||
# (*without* a default value on the class body)
|
||||
# should be understood as satisfying a protocol that has an attribute member `x`
|
||||
# even if the protocol's `x` member has a default value on the class body.
|
||||
#
|
||||
# See <https://github.com/python/typing/blob/d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc/conformance/tests/protocols_definition.py#L56-L79>.
|
||||
#
|
||||
# The implications of this for meta-protocols are not clearly spelled out, however,
|
||||
# and the fact that attribute members on protocols can have defaults is only mentioned
|
||||
# in a throwaway comment in the spec's prose.
|
||||
static_assert(is_subtype_of(Foo, HasXWithDefault))
|
||||
static_assert(is_assignable_to(Foo, HasXWithDefault))
|
||||
static_assert(is_subtype_of(Qux, HasXWithDefault))
|
||||
static_assert(is_assignable_to(Qux, HasXWithDefault))
|
||||
|
||||
class HasClassVarX(Protocol):
|
||||
x: ClassVar[int]
|
||||
@@ -747,9 +799,9 @@ def f(arg: HasXWithDefault):
|
||||
```
|
||||
|
||||
Assignments in a class body of a protocol -- of any kind -- are not permitted by ty 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).
|
||||
symbol being assigned to is also explicitly declared in the body of the protocol class or one of its
|
||||
superclasses. 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.
|
||||
@@ -773,24 +825,75 @@ class LotsOfBindings(Protocol):
|
||||
|
||||
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)
|
||||
e = 72 # error: [ambiguous-protocol-member]
|
||||
|
||||
f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared)
|
||||
# error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `f: int = ...`"
|
||||
# error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `g: int = ...`"
|
||||
f, g = (1, 2)
|
||||
|
||||
h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared)
|
||||
h: int = (i := 3) # error: [ambiguous-protocol-member]
|
||||
|
||||
for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared)
|
||||
for j in range(42): # error: [ambiguous-protocol-member]
|
||||
pass
|
||||
|
||||
with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared)
|
||||
with MyContext() as k: # error: [ambiguous-protocol-member]
|
||||
pass
|
||||
|
||||
match object():
|
||||
case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared)
|
||||
case l: # error: [ambiguous-protocol-member]
|
||||
...
|
||||
|
||||
# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]]
|
||||
reveal_type(get_protocol_members(LotsOfBindings))
|
||||
|
||||
class Foo(Protocol):
|
||||
a: int
|
||||
|
||||
class Bar(Foo, Protocol):
|
||||
a = 42 # fine, because it's declared in the superclass
|
||||
|
||||
reveal_type(get_protocol_members(Bar)) # revealed: frozenset[Literal["a"]]
|
||||
```
|
||||
|
||||
A binding-without-declaration will not be reported if it occurs in a branch that we can statically
|
||||
determine to be unreachable. The reason is that we don't consider it to be a protocol member at all
|
||||
if all definitions for the variable are in unreachable blocks:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
class Protocol694(Protocol):
|
||||
if sys.version_info > (3, 694):
|
||||
x = 42 # no error!
|
||||
```
|
||||
|
||||
If there are multiple bindings of the variable in the class body, however, and at least one of the
|
||||
bindings occurs in a block of code that is understood to be (possibly) reachable, a diagnostic will
|
||||
be reported. The diagnostic will be attached to the first binding that occurs in the class body,
|
||||
even if that first definition occurs in an unreachable block:
|
||||
|
||||
```py
|
||||
class Protocol695(Protocol):
|
||||
if sys.version_info > (3, 695):
|
||||
x = 42
|
||||
else:
|
||||
x = 42
|
||||
|
||||
x = 56 # error: [ambiguous-protocol-member]
|
||||
```
|
||||
|
||||
In order for the variable to be considered declared, the declaration of the variable must also take
|
||||
place in a block of code that is understood to be (possibly) reachable:
|
||||
|
||||
```py
|
||||
class Protocol696(Protocol):
|
||||
if sys.version_info > (3, 696):
|
||||
x: int
|
||||
else:
|
||||
x = 42 # error: [ambiguous-protocol-member]
|
||||
y: int
|
||||
|
||||
y = 56 # no error
|
||||
```
|
||||
|
||||
Attribute members are allowed to have assignments in methods on the protocol class, just like
|
||||
@@ -893,6 +996,40 @@ static_assert(not is_assignable_to(HasX, Foo))
|
||||
static_assert(not is_subtype_of(HasX, Foo))
|
||||
```
|
||||
|
||||
## Diagnostics for protocols with invalid attribute members
|
||||
|
||||
This is a short appendix to the previous section with the `snapshot-diagnostics` directive enabled
|
||||
(enabling snapshots for the previous section in its entirety would lead to a huge snapshot, since
|
||||
it's a large section).
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
class A(Protocol):
|
||||
# The `x` and `y` members attempt to use Python-2-style type comments
|
||||
# to indicate that the type should be `int | None` and `str` respectively,
|
||||
# but we don't support those
|
||||
|
||||
# error: [ambiguous-protocol-member]
|
||||
a = None # type: int
|
||||
# error: [ambiguous-protocol-member]
|
||||
b = ... # type: str
|
||||
|
||||
if coinflip():
|
||||
c = 1 # error: [ambiguous-protocol-member]
|
||||
else:
|
||||
c = 2
|
||||
|
||||
# error: [ambiguous-protocol-member]
|
||||
for d in range(42):
|
||||
pass
|
||||
```
|
||||
|
||||
## Equivalence of protocols
|
||||
|
||||
Two protocols are considered equivalent types if they specify the same interface, even if they have
|
||||
@@ -1923,7 +2060,7 @@ def _(r: Recursive):
|
||||
reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]]
|
||||
reveal_type(r.callable1) # revealed: (int, /) -> Recursive
|
||||
reveal_type(r.callable2) # revealed: (Recursive, /) -> int
|
||||
reveal_type(r.subtype_of) # revealed: type[Recursive]
|
||||
reveal_type(r.subtype_of) # revealed: @Todo(type[T] for protocols)
|
||||
reveal_type(r.generic) # revealed: GenericC[Recursive]
|
||||
reveal_type(r.method(r)) # revealed: Recursive
|
||||
reveal_type(r.nested) # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> Recursive)
|
||||
@@ -2061,6 +2198,86 @@ def f(value: Iterator):
|
||||
cast(Iterator, value) # error: [redundant-cast]
|
||||
```
|
||||
|
||||
## Meta-protocols
|
||||
|
||||
Where `P` is a protocol type, a class object `N` can be said to inhabit the type `type[P]` if:
|
||||
|
||||
- All `ClassVar` members on `P` exist on the class object `N`
|
||||
- All method members on `P` exist on the class object `N`
|
||||
- Instantiating `N` creates an object that would satisfy the protocol `P`
|
||||
|
||||
Currently meta-protocols are not fully supported by ty, but we try to keep false positives to a
|
||||
minimum in the meantime.
|
||||
|
||||
```py
|
||||
from typing import Protocol, ClassVar
|
||||
from ty_extensions import static_assert, is_assignable_to, TypeOf, is_subtype_of
|
||||
|
||||
class Foo(Protocol):
|
||||
x: int
|
||||
y: ClassVar[str]
|
||||
def method(self) -> bytes: ...
|
||||
|
||||
def _(f: type[Foo]):
|
||||
reveal_type(f) # revealed: type[@Todo(type[T] for protocols)]
|
||||
|
||||
# TODO: we should emit `unresolved-attribute` here: although we would accept this for a
|
||||
# nominal class, we would see any class `N` as inhabiting `Foo` if it had an implicit
|
||||
# instance attribute `x`, and implicit instance attributes are rarely bound on the class
|
||||
# object.
|
||||
reveal_type(f.x) # revealed: @Todo(type[T] for protocols)
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(f.y) # revealed: @Todo(type[T] for protocols)
|
||||
f.y = "foo" # fine
|
||||
|
||||
# TODO: should be `Callable[[Foo], bytes]`
|
||||
reveal_type(f.method) # revealed: @Todo(type[T] for protocols)
|
||||
|
||||
class Bar: ...
|
||||
|
||||
# TODO: these should pass
|
||||
static_assert(not is_assignable_to(type[Bar], type[Foo])) # error: [static-assert-error]
|
||||
static_assert(not is_assignable_to(TypeOf[Bar], type[Foo])) # error: [static-assert-error]
|
||||
|
||||
class Baz:
|
||||
x: int
|
||||
y: ClassVar[str] = "foo"
|
||||
def method(self) -> bytes:
|
||||
return b"foo"
|
||||
|
||||
static_assert(is_assignable_to(type[Baz], type[Foo]))
|
||||
static_assert(is_assignable_to(TypeOf[Baz], type[Foo]))
|
||||
|
||||
# TODO: these should pass
|
||||
static_assert(is_subtype_of(type[Baz], type[Foo])) # error: [static-assert-error]
|
||||
static_assert(is_subtype_of(TypeOf[Baz], type[Foo])) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
## Regression test for `ClassVar` members in stubs
|
||||
|
||||
In an early version of our protocol implementation, we didn't retain the `ClassVar` qualifier for
|
||||
protocols defined in stub files.
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import ClassVar, Protocol
|
||||
|
||||
class Foo(Protocol):
|
||||
x: ClassVar[int]
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from stub import Foo
|
||||
from ty_extensions import reveal_protocol_interface
|
||||
|
||||
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
|
||||
reveal_protocol_interface(Foo)
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
Add tests for:
|
||||
|
||||
@@ -49,6 +49,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
|
||||
35 | y: str
|
||||
36 | _2: KW_ONLY
|
||||
37 | z: float
|
||||
38 | from dataclasses import dataclass, KW_ONLY
|
||||
39 |
|
||||
40 | @dataclass
|
||||
41 | class D:
|
||||
42 | x: int
|
||||
43 | _: KW_ONLY
|
||||
44 | y: str
|
||||
45 |
|
||||
46 | @dataclass
|
||||
47 | class E(D):
|
||||
48 | z: bytes
|
||||
49 |
|
||||
50 | # This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only)
|
||||
51 | E(1, b"foo", y="foo")
|
||||
52 |
|
||||
53 | reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -141,3 +157,15 @@ info: `KW_ONLY` fields: `_1`, `_2`
|
||||
info: rule `duplicate-kw-only` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info[revealed-type]: Revealed type
|
||||
--> src/mdtest_snippet.py:53:13
|
||||
|
|
||||
51 | E(1, b"foo", y="foo")
|
||||
52 |
|
||||
53 | reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
|
||||
| ^^^^^^^^^^ `(self: E, x: int, z: bytes, *, y: str) -> None`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Basic
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | async def main() -> None:
|
||||
2 | await 1 # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `Literal[1]` is not awaitable
|
||||
--> src/mdtest_snippet.py:2:11
|
||||
|
|
||||
1 | async def main() -> None:
|
||||
2 | await 1 # error: [invalid-await]
|
||||
| ^
|
||||
|
|
||||
::: stdlib/builtins.pyi:337:7
|
||||
|
|
||||
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
336 |
|
||||
337 | class int:
|
||||
| --- type defined here
|
||||
338 | """int([x]) -> integer
|
||||
339 | int(x, base=10) -> integer
|
||||
|
|
||||
info: `__await__` is missing
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with missing `__await__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class MissingAwait:
|
||||
2 | pass
|
||||
3 |
|
||||
4 | async def main() -> None:
|
||||
5 | await MissingAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `MissingAwait` is not awaitable
|
||||
--> src/mdtest_snippet.py:5:11
|
||||
|
|
||||
4 | async def main() -> None:
|
||||
5 | await MissingAwait() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:1:7
|
||||
|
|
||||
1 | class MissingAwait:
|
||||
| ------------ type defined here
|
||||
2 | pass
|
||||
|
|
||||
info: `__await__` is missing
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with possibly unbound `__await__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from datetime import datetime
|
||||
2 |
|
||||
3 | class PossiblyUnbound:
|
||||
4 | if datetime.today().weekday() == 0:
|
||||
5 | def __await__(self):
|
||||
6 | yield
|
||||
7 |
|
||||
8 | async def main() -> None:
|
||||
9 | await PossiblyUnbound() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `PossiblyUnbound` is not awaitable
|
||||
--> src/mdtest_snippet.py:9:11
|
||||
|
|
||||
8 | async def main() -> None:
|
||||
9 | await PossiblyUnbound() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:5:13
|
||||
|
|
||||
3 | class PossiblyUnbound:
|
||||
4 | if datetime.today().weekday() == 0:
|
||||
5 | def __await__(self):
|
||||
| --------------- method defined here
|
||||
6 | yield
|
||||
|
|
||||
info: `__await__` is possibly unbound
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Invalid union return type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | import typing
|
||||
2 | from datetime import datetime
|
||||
3 |
|
||||
4 | class UnawaitableUnion:
|
||||
5 | if datetime.today().weekday() == 6:
|
||||
6 |
|
||||
7 | def __await__(self) -> typing.Generator[typing.Any, None, None]:
|
||||
8 | yield
|
||||
9 | else:
|
||||
10 |
|
||||
11 | def __await__(self) -> int:
|
||||
12 | return 5
|
||||
13 |
|
||||
14 | async def main() -> None:
|
||||
15 | await UnawaitableUnion() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `UnawaitableUnion` is not awaitable
|
||||
--> src/mdtest_snippet.py:15:11
|
||||
|
|
||||
14 | async def main() -> None:
|
||||
15 | await UnawaitableUnion() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__await__` returns `Generator[Any, None, None] | int`, which is not a valid iterator
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Non-callable `__await__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NonCallableAwait:
|
||||
2 | __await__ = 42
|
||||
3 |
|
||||
4 | async def main() -> None:
|
||||
5 | await NonCallableAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `NonCallableAwait` is not awaitable
|
||||
--> src/mdtest_snippet.py:5:11
|
||||
|
|
||||
4 | async def main() -> None:
|
||||
5 | await NonCallableAwait() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__await__` is possibly not callable
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - `__await__` definition with extra arguments
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class InvalidAwaitArgs:
|
||||
2 | def __await__(self, value: int):
|
||||
3 | yield value
|
||||
4 |
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitArgs() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `InvalidAwaitArgs` is not awaitable
|
||||
--> src/mdtest_snippet.py:6:11
|
||||
|
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitArgs() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:2:18
|
||||
|
|
||||
1 | class InvalidAwaitArgs:
|
||||
2 | def __await__(self, value: int):
|
||||
| ------------------ parameters here
|
||||
3 | yield value
|
||||
|
|
||||
info: `__await__` requires arguments and cannot be called implicitly
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - `__await__` definition with explicit invalid return type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class InvalidAwaitReturn:
|
||||
2 | def __await__(self) -> int:
|
||||
3 | return 5
|
||||
4 |
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitReturn() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `InvalidAwaitReturn` is not awaitable
|
||||
--> src/mdtest_snippet.py:6:11
|
||||
|
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitReturn() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | class InvalidAwaitReturn:
|
||||
2 | def __await__(self) -> int:
|
||||
| ---------------------- method defined here
|
||||
3 | return 5
|
||||
|
|
||||
info: `__await__` returns `int`, which is not a valid iterator
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: named_tuple.md - `NamedTuple` - `typing.NamedTuple` - Definition
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import NamedTuple
|
||||
2 |
|
||||
3 | class Location(NamedTuple):
|
||||
4 | altitude: float = 0.0
|
||||
5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitude` defined here without a default value"
|
||||
6 | latitude: float
|
||||
7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitude` defined here without a default value"
|
||||
8 | longitude: float
|
||||
9 |
|
||||
10 | class StrangeLocation(NamedTuple):
|
||||
11 | altitude: float
|
||||
12 | altitude: float = 0.0
|
||||
13 | altitude: float
|
||||
14 | altitude: float = 0.0
|
||||
15 | latitude: float # error: [invalid-named-tuple]
|
||||
16 | longitude: float # error: [invalid-named-tuple]
|
||||
17 |
|
||||
18 | class VeryStrangeLocation(NamedTuple):
|
||||
19 | altitude: float = 0.0
|
||||
20 | latitude: float # error: [invalid-named-tuple]
|
||||
21 | longitude: float # error: [invalid-named-tuple]
|
||||
22 | altitude: float = 0.0
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
|
||||
--> src/mdtest_snippet.py:4:5
|
||||
|
|
||||
3 | class Location(NamedTuple):
|
||||
4 | altitude: float = 0.0
|
||||
| --------------------- Earlier field `altitude` defined here with a default value
|
||||
5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitud…
|
||||
6 | latitude: float
|
||||
| ^^^^^^^^ Field `latitude` defined here without a default value
|
||||
7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitu…
|
||||
8 | longitude: float
|
||||
|
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
|
||||
--> src/mdtest_snippet.py:4:5
|
||||
|
|
||||
3 | class Location(NamedTuple):
|
||||
4 | altitude: float = 0.0
|
||||
| --------------------- Earlier field `altitude` defined here with a default value
|
||||
5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitu…
|
||||
6 | latitude: float
|
||||
7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longit…
|
||||
8 | longitude: float
|
||||
| ^^^^^^^^^ Field `longitude` defined here without a default value
|
||||
9 |
|
||||
10 | class StrangeLocation(NamedTuple):
|
||||
|
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
|
||||
--> src/mdtest_snippet.py:14:5
|
||||
|
|
||||
12 | altitude: float = 0.0
|
||||
13 | altitude: float
|
||||
14 | altitude: float = 0.0
|
||||
| --------------------- Earlier field `altitude` defined here with a default value
|
||||
15 | latitude: float # error: [invalid-named-tuple]
|
||||
| ^^^^^^^^ Field `latitude` defined here without a default value
|
||||
16 | longitude: float # error: [invalid-named-tuple]
|
||||
|
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
|
||||
--> src/mdtest_snippet.py:14:5
|
||||
|
|
||||
12 | altitude: float = 0.0
|
||||
13 | altitude: float
|
||||
14 | altitude: float = 0.0
|
||||
| --------------------- Earlier field `altitude` defined here with a default value
|
||||
15 | latitude: float # error: [invalid-named-tuple]
|
||||
16 | longitude: float # error: [invalid-named-tuple]
|
||||
| ^^^^^^^^^ Field `longitude` defined here without a default value
|
||||
17 |
|
||||
18 | class VeryStrangeLocation(NamedTuple):
|
||||
|
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
|
||||
--> src/mdtest_snippet.py:20:5
|
||||
|
|
||||
18 | class VeryStrangeLocation(NamedTuple):
|
||||
19 | altitude: float = 0.0
|
||||
20 | latitude: float # error: [invalid-named-tuple]
|
||||
| ^^^^^^^^ Field `latitude` defined here without a default value
|
||||
21 | longitude: float # error: [invalid-named-tuple]
|
||||
22 | altitude: float = 0.0
|
||||
|
|
||||
info: Earlier field `altitude` was defined with a default value
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
|
||||
--> src/mdtest_snippet.py:21:5
|
||||
|
|
||||
19 | altitude: float = 0.0
|
||||
20 | latitude: float # error: [invalid-named-tuple]
|
||||
21 | longitude: float # error: [invalid-named-tuple]
|
||||
| ^^^^^^^^^ Field `longitude` defined here without a default value
|
||||
22 | altitude: float = 0.0
|
||||
|
|
||||
info: Earlier field `altitude` was defined with a default value
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user