Compare commits
58 Commits
0.6.9
...
dhruv/work
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d0468137c | ||
|
|
c6b311c546 | ||
|
|
b16f665a81 | ||
|
|
d77480768d | ||
|
|
5fa82fb0cd | ||
|
|
74bf4b0653 | ||
|
|
5f65e842e8 | ||
|
|
72ac6cd5a5 | ||
|
|
04b636cba2 | ||
|
|
6048f331d9 | ||
|
|
93097f1c53 | ||
|
|
9bb4722ebf | ||
|
|
5caabe54b6 | ||
|
|
814ab47582 | ||
|
|
c3a3622e30 | ||
|
|
4ef422d3b4 | ||
|
|
58bc981677 | ||
|
|
dd5018ac55 | ||
|
|
63df94b521 | ||
|
|
e4c0dd6f96 | ||
|
|
3111dce5b4 | ||
|
|
8445e4725c | ||
|
|
defdc4dd8e | ||
|
|
46bc69d1d4 | ||
|
|
3209953276 | ||
|
|
6ae833e0c7 | ||
|
|
a3dc5c0529 | ||
|
|
d6b24b690a | ||
|
|
5b4afd30ca | ||
|
|
b9827a4122 | ||
|
|
93eff7f174 | ||
|
|
fc661e193a | ||
|
|
42fcbef876 | ||
|
|
71b52b83e4 | ||
|
|
fb90f5a13d | ||
|
|
d7484e6942 | ||
|
|
14ee5dbfde | ||
|
|
27ac34d683 | ||
|
|
31ca1c3064 | ||
|
|
646e4136d7 | ||
|
|
58a11b33da | ||
|
|
7856e90a2c | ||
|
|
98878c9bf2 | ||
|
|
73aa6ea417 | ||
|
|
38d872ea4c | ||
|
|
824def2194 | ||
|
|
2ab78dd6a5 | ||
|
|
03fa7f64dd | ||
|
|
43330225be | ||
|
|
383d9d9f6e | ||
|
|
8108f83810 | ||
|
|
f1205177fd | ||
|
|
1c2cafc101 | ||
|
|
7c5a7d909c | ||
|
|
2a365bb278 | ||
|
|
020f4d4a54 | ||
|
|
888930b7d3 | ||
|
|
d726f09cf0 |
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -148,7 +148,7 @@ jobs:
|
||||
# sync, not just public items. Eventually we should do this for all
|
||||
# crates; for now add crates here as they are warning-clean to prevent
|
||||
# regression.
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p ruff_db --document-private-items
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
@@ -518,6 +518,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
@@ -525,13 +527,15 @@ jobs:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: pip install -r docs/requirements-insiders.txt
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
- name: "Install dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
|
||||
run: pip install -r docs/requirements.txt
|
||||
run: uv pip install -r docs/requirements.txt --system
|
||||
- name: "Update README File"
|
||||
run: python scripts/transform_readme.py --target mkdocs
|
||||
- name: "Generate docs"
|
||||
@@ -608,7 +612,7 @@ jobs:
|
||||
just test
|
||||
|
||||
benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
|
||||
@@ -28,6 +28,7 @@ repos:
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
- mdformat-footnote
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/formatter/black\.md
|
||||
@@ -45,7 +46,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.24.6
|
||||
rev: v1.25.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -59,7 +60,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.8
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
195
Cargo.lock
generated
195
Cargo.lock
generated
@@ -36,12 +36,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -353,9 +347,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.18"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -363,9 +357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.18"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -714,7 +708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
@@ -728,7 +722,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
@@ -1026,16 +1020,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1127,7 +1126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1147,7 +1146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1312,9 +1311,9 @@ checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -1353,9 +1352,9 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10293a04a48e8b0cb2cc825a93b83090e527bffd3c897a0255ad7bc96079e920"
|
||||
checksum = "1586dd7a857d8a61a577afde1a24cc9573ff549eff092d5ce968b1ec93cc61b6"
|
||||
dependencies = [
|
||||
"chic",
|
||||
"libcst_derive",
|
||||
@@ -1626,9 +1625,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -1691,9 +1690,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "path-absolutize"
|
||||
@@ -1721,15 +1720,15 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
|
||||
|
||||
[[package]]
|
||||
name = "peg"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "400bcab7d219c38abf8bd7cc2054eb9bbbd4312d66f6a5557d572a203f646f61"
|
||||
checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f"
|
||||
dependencies = [
|
||||
"peg-macros",
|
||||
"peg-runtime",
|
||||
@@ -1737,9 +1736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "peg-macros"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e61cce859b76d19090f62da50a9fe92bab7c2a5f09e183763559a2ac392c90"
|
||||
checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426"
|
||||
dependencies = [
|
||||
"peg-runtime",
|
||||
"proc-macro2",
|
||||
@@ -1748,9 +1747,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "peg-runtime"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36bae92c60fa2398ce4678b98b2c4b5a7c61099961ca1fa305aec04a9ad28922"
|
||||
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
|
||||
|
||||
[[package]]
|
||||
name = "pep440_rs"
|
||||
@@ -1944,9 +1943,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2081,10 +2080,14 @@ dependencies = [
|
||||
"camino",
|
||||
"compact_str",
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.0",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"memchr",
|
||||
"ordermap",
|
||||
"red_knot_test",
|
||||
"red_knot_vendored",
|
||||
"rstest",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
@@ -2127,6 +2130,26 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_test"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"colored",
|
||||
"once_cell",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_vendored"
|
||||
version = "0.0.0"
|
||||
@@ -2247,6 +2270,12 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
@@ -2262,6 +2291,33 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936"
|
||||
dependencies = [
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.9"
|
||||
@@ -2412,7 +2468,6 @@ dependencies = [
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_python_trivia",
|
||||
"ruff_workspace",
|
||||
"schemars",
|
||||
@@ -2778,6 +2833,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -2885,6 +2941,15 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
@@ -2945,7 +3010,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=4a7c955255e707e64e43f3ce5eabb771ae067768#4a7c955255e707e64e43f3ce5eabb771ae067768"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
@@ -2965,12 +3030,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=4a7c955255e707e64e43f3ce5eabb771ae067768#4a7c955255e707e64e43f3ce5eabb771ae067768"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=4a7c955255e707e64e43f3ce5eabb771ae067768#4a7c955255e707e64e43f3ce5eabb771ae067768"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b14be5c0392f4c55eca60b92e457a35549372382#b14be5c0392f4c55eca60b92e457a35549372382"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3030,6 +3095,12 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@@ -3115,9 +3186,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.9.0"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
|
||||
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3126,9 +3197,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.9.0"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -3281,12 +3352,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
||||
checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3788,9 +3859,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -3799,9 +3870,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -3814,9 +3885,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.43"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
|
||||
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -3826,9 +3897,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3836,9 +3907,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3849,15 +3920,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.43"
|
||||
version = "0.3.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9"
|
||||
checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -3870,9 +3941,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.43"
|
||||
version = "0.3.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021"
|
||||
checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
71
Cargo.toml
71
Cargo.toml
@@ -39,6 +39,7 @@ ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_workspace = { path = "crates/red_knot_workspace", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
@@ -72,7 +73,10 @@ filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = "0.14.3"
|
||||
hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
"raw-entry",
|
||||
"inline-more",
|
||||
] }
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
@@ -90,7 +94,7 @@ libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
|
||||
"proposed",
|
||||
"proposed",
|
||||
] }
|
||||
matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
@@ -111,8 +115,9 @@ quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rstest = { version = "0.22.0", default-features = false }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "4a7c955255e707e64e43f3ce5eabb771ae067768" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b14be5c0392f4c55eca60b92e457a35549372382" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -120,7 +125,7 @@ serde-wasm-bindgen = { version = "0.6.4" }
|
||||
serde_json = { version = "1.0.113" }
|
||||
serde_test = { version = "1.0.152" }
|
||||
serde_with = { version = "3.6.0", default-features = false, features = [
|
||||
"macros",
|
||||
"macros",
|
||||
] }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
@@ -137,7 +142,10 @@ toml = { version = "0.8.11" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-flame = { version = "0.2.0" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["env-filter", "fmt"] }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
] }
|
||||
tracing-tree = { version = "0.4.0" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
@@ -148,10 +156,10 @@ unicode-normalization = { version = "0.1.23" }
|
||||
ureq = { version = "2.9.6" }
|
||||
url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"js",
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"js",
|
||||
] }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.92" }
|
||||
@@ -162,7 +170,10 @@ zip = { version = "0.6.6", default-features = false }
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(fuzzing)", "cfg(codspeed)"] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
"cfg(fuzzing)",
|
||||
"cfg(codspeed)",
|
||||
] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -2 }
|
||||
@@ -245,23 +256,23 @@ windows-archive = ".zip"
|
||||
unix-archive = ".tar.gz"
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
]
|
||||
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
|
||||
auto-includes = false
|
||||
@@ -280,7 +291,11 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
post-announce-jobs = [
|
||||
"./notify-dependents",
|
||||
"./publish-docs",
|
||||
"./publish-playground",
|
||||
]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
|
||||
@@ -501,7 +501,10 @@ fn directory_moved_to_workspace() -> anyhow::Result<()> {
|
||||
.with_context(|| "Failed to create __init__.py")?;
|
||||
std::fs::write(a_original_path.as_std_path(), "").with_context(|| "Failed to create a.py")?;
|
||||
|
||||
let sub_a_module = resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap());
|
||||
let sub_a_module = resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(sub_a_module, None);
|
||||
assert_eq!(
|
||||
@@ -525,7 +528,11 @@ fn directory_moved_to_workspace() -> anyhow::Result<()> {
|
||||
.expect("a.py to exist");
|
||||
|
||||
// `import sub.a` should now resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_some());
|
||||
|
||||
assert_eq!(
|
||||
case.collect_package_files(&case.workspace_path("bar.py")),
|
||||
@@ -544,7 +551,11 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
])?;
|
||||
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_some());
|
||||
|
||||
let sub_path = case.workspace_path("sub");
|
||||
let init_file = case
|
||||
@@ -569,7 +580,11 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_none());
|
||||
|
||||
assert!(!init_file.exists(case.db()));
|
||||
assert!(!a_file.exists(case.db()));
|
||||
@@ -592,10 +607,14 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
|
||||
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("foo.baz").unwrap()
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_some());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("foo.baz").unwrap()
|
||||
)
|
||||
.is_none());
|
||||
|
||||
@@ -623,11 +642,15 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_none());
|
||||
// `import foo.baz` should now resolve
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("foo.baz").unwrap()
|
||||
&ModuleName::new_static("foo.baz").unwrap()
|
||||
)
|
||||
.is_some());
|
||||
|
||||
@@ -665,7 +688,11 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_some());
|
||||
|
||||
let sub_path = case.workspace_path("sub");
|
||||
|
||||
@@ -688,7 +715,11 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
assert!(resolve_module(
|
||||
case.db().upcast(),
|
||||
&ModuleName::new_static("sub.a").unwrap()
|
||||
)
|
||||
.is_none());
|
||||
|
||||
assert!(!init_file.exists(case.db()));
|
||||
assert!(!a_file.exists(case.db()));
|
||||
@@ -710,7 +741,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
let site_packages = case.root_path().join("site_packages");
|
||||
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), ModuleName::new("a").unwrap()),
|
||||
resolve_module(case.db(), &ModuleName::new("a").unwrap()),
|
||||
None
|
||||
);
|
||||
|
||||
@@ -720,7 +751,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert_eq!(
|
||||
case.collect_package_files(&case.workspace_path("bar.py")),
|
||||
&[case.system_file(case.workspace_path("bar.py")).unwrap()]
|
||||
@@ -736,7 +767,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
let site_packages = case.workspace_path("site_packages");
|
||||
std::fs::create_dir_all(site_packages.as_std_path())?;
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("a").unwrap()).is_none());
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_none());
|
||||
|
||||
// Register site-packages as a search path.
|
||||
case.update_search_path_settings(SearchPathConfiguration {
|
||||
@@ -751,7 +782,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -805,7 +836,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
|
||||
// Unset the custom typeshed directory.
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), ModuleName::new("os").unwrap()),
|
||||
resolve_module(case.db(), &ModuleName::new("os").unwrap()),
|
||||
None
|
||||
);
|
||||
|
||||
@@ -820,7 +851,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db(), ModuleName::new("os").unwrap()).is_some());
|
||||
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1044,7 +1075,7 @@ mod unix {
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
&ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_workspace = case.workspace_path("bar/baz.py");
|
||||
@@ -1125,7 +1156,7 @@ mod unix {
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
&ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let bar_baz = case.workspace_path("bar/baz.py");
|
||||
@@ -1229,7 +1260,7 @@ mod unix {
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
&ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_site_packages_path =
|
||||
|
||||
@@ -24,6 +24,7 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
itertools = { workspace = true}
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -33,14 +34,17 @@ hashbrown = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["os", "testing"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
4
crates/red_knot_python_semantic/build.rs
Normal file
4
crates/red_knot_python_semantic/build.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
/// Rebuild the crate if a test file is added or removed from
|
||||
pub fn main() {
|
||||
println!("cargo:rerun-if-changed=resources/mdtest");
|
||||
}
|
||||
4
crates/red_knot_python_semantic/resources/README.md
Normal file
4
crates/red_knot_python_semantic/resources/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking;
|
||||
executed by the `tests/mdtest.rs` integration test.
|
||||
|
||||
See `crates/red_knot_test/README.md` for documentation of this test format.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Assignment with annotations
|
||||
|
||||
## Annotation only transparent to local inference
|
||||
|
||||
```py
|
||||
x = 1
|
||||
x: int
|
||||
y = x
|
||||
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Violates own annotation
|
||||
|
||||
```py
|
||||
x: int = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
|
||||
```
|
||||
|
||||
## Violates previous annotation
|
||||
|
||||
```py
|
||||
x: int
|
||||
x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# Multi-target assignment
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
x = y = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Unbound
|
||||
|
||||
## Maybe unbound
|
||||
|
||||
```py
|
||||
if flag:
|
||||
y = 3
|
||||
x = y
|
||||
reveal_type(x) # revealed: Unbound | Literal[3]
|
||||
```
|
||||
|
||||
## Unbound
|
||||
|
||||
```py
|
||||
x = foo; foo = 1
|
||||
reveal_type(x) # revealed: Unbound
|
||||
```
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Class variables can reference global variables unless overridden within the class scope.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
class C:
|
||||
y = x
|
||||
if flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(C.x) # revealed: Unbound | Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# Walrus operator
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
x = (y := 1) + 1
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Walrus self-addition
|
||||
|
||||
```py
|
||||
x = 0
|
||||
(x := x + 1)
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Class attributes
|
||||
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
if flag:
|
||||
class C:
|
||||
x = 1
|
||||
else:
|
||||
class C:
|
||||
x = 2
|
||||
|
||||
y = C.x
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
## Binary operations on integers
|
||||
|
||||
## Basic Arithmetic
|
||||
|
||||
```py
|
||||
a = 2 + 1
|
||||
b = a - 4
|
||||
c = a * b
|
||||
d = c // 3
|
||||
e = c / 3
|
||||
f = 5 % 3
|
||||
|
||||
reveal_type(a) # revealed: Literal[3]
|
||||
reveal_type(b) # revealed: Literal[-1]
|
||||
reveal_type(c) # revealed: Literal[-3]
|
||||
reveal_type(d) # revealed: Literal[-1]
|
||||
reveal_type(e) # revealed: float
|
||||
reveal_type(f) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
```py
|
||||
# TODO: `a` should be `int` and `e` should be `float` once we support inference.
|
||||
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
|
||||
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
|
||||
d = int() / 0 # error: "Cannot divide object of type `int` by zero"
|
||||
e = 1.0 / 0 # error: "Cannot divide object of type `float` by zero"
|
||||
|
||||
reveal_type(a) # revealed: float
|
||||
reveal_type(b) # revealed: int
|
||||
reveal_type(c) # revealed: int
|
||||
reveal_type(d) # revealed: @Todo
|
||||
reveal_type(e) # revealed: @Todo
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Callable instance
|
||||
|
||||
## Dunder call
|
||||
|
||||
```py
|
||||
class Multiplier:
|
||||
def __init__(self, factor: float):
|
||||
self.factor = factor
|
||||
|
||||
def __call__(self, number: float) -> float:
|
||||
return number * self.factor
|
||||
|
||||
a = Multiplier(2.0)(3.0)
|
||||
|
||||
class Unit: ...
|
||||
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
|
||||
reveal_type(a) # revealed: float
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Constructor
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# Call expression
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def get_int() -> int:
|
||||
return 42
|
||||
|
||||
x = get_int()
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Async
|
||||
|
||||
```py
|
||||
async def get_int_async() -> int:
|
||||
return 42
|
||||
|
||||
x = get_int_async()
|
||||
|
||||
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
|
||||
reveal_type(x) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Decorated
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
return foo
|
||||
|
||||
@decorator
|
||||
def bar() -> str:
|
||||
return 'bar'
|
||||
|
||||
x = bar()
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(x) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
# Unions in calls
|
||||
|
||||
## Union of return types
|
||||
|
||||
```py
|
||||
if flag:
|
||||
def f() -> int:
|
||||
return 1
|
||||
else:
|
||||
def f() -> str:
|
||||
return 'foo'
|
||||
|
||||
x = f()
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## Calling with an unknown union
|
||||
|
||||
```py
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
|
||||
if flag:
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f()
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Non-callable elements in a union
|
||||
|
||||
Calling a union with a non-callable element should emit a diagnostic.
|
||||
|
||||
```py
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
|
||||
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
|
||||
|
||||
```py
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = 'foo'
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## All non-callable union elements
|
||||
|
||||
Calling a union with no callable elements can emit a simpler diagnostic.
|
||||
|
||||
```py
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = 'foo'
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
### Comparison: Byte literals
|
||||
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects
|
||||
inferred as having `Literal` bytes types:
|
||||
|
||||
```py
|
||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" != b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
|
||||
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" <= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" <= b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
|
||||
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" >= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" >= b"abd") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"" in b"") # revealed: Literal[True]
|
||||
reveal_type(b"" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"") # revealed: Literal[False]
|
||||
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"d" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"\x81\x82" in b"\x80\x81\x82") # revealed: Literal[True]
|
||||
reveal_type(b"\x82\x83" in b"\x80\x81\x82") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" is b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" is not b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# Comparing integers
|
||||
|
||||
## Integer literals
|
||||
|
||||
```py
|
||||
a = 1 == 1 == True
|
||||
b = 1 == 1 == 2 == 4
|
||||
c = False < True <= 2 < 3 != 6
|
||||
d = 1 < 1
|
||||
e = 1 > 1
|
||||
f = 1 is 1
|
||||
g = 1 is not 1
|
||||
h = 1 is 2
|
||||
i = 1 is not 7
|
||||
j = 1 <= "" and 0 < 1
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[True]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: Literal[False]
|
||||
reveal_type(f) # revealed: bool
|
||||
reveal_type(g) # revealed: bool
|
||||
reveal_type(h) # revealed: Literal[False]
|
||||
reveal_type(i) # revealed: Literal[True]
|
||||
reveal_type(j) # revealed: @Todo | Literal[True]
|
||||
```
|
||||
|
||||
## Integer instance
|
||||
|
||||
```py
|
||||
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
||||
def int_instance() -> int: ...
|
||||
a = 1 == int_instance()
|
||||
b = 9 < int_instance()
|
||||
c = int_instance() < int_instance()
|
||||
|
||||
reveal_type(a) # revealed: @Todo
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: bool
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
# Non boolean returns
|
||||
|
||||
Walking through examples:
|
||||
|
||||
- `a = A() < B() < C()`
|
||||
|
||||
1. `A() < B() and B() < C()` - split in N comparison
|
||||
1. `A()` and `B()` - evaluate outcome types
|
||||
1. `bool` and `bool` - evaluate truthiness
|
||||
1. `A | B` - union of "first true" types
|
||||
|
||||
- `b = 0 < 1 < A() < 3`
|
||||
|
||||
1. `0 < 1 and 1 < A() and A() < 3` - split in N comparison
|
||||
1. `True` and `bool` and `A` - evaluate outcome types
|
||||
1. `True` and `bool` and `bool` - evaluate truthiness
|
||||
1. `bool | A` - union of "true" types
|
||||
|
||||
- `c = 10 < 0 < A() < B() < C()` short-circuit to False
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
class A:
|
||||
def __lt__(self, other) -> A: ...
|
||||
class B:
|
||||
def __lt__(self, other) -> B: ...
|
||||
class C:
|
||||
def __lt__(self, other) -> C: ...
|
||||
|
||||
a = A() < B() < C()
|
||||
b = 0 < 1 < A() < 3
|
||||
c = 10 < 0 < A() < B() < C()
|
||||
|
||||
reveal_type(a) # revealed: A | B
|
||||
reveal_type(b) # revealed: bool | A
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Comparing strings
|
||||
|
||||
## String literals
|
||||
|
||||
```py
|
||||
def str_instance() -> str: ...
|
||||
a = "abc" == "abc"
|
||||
b = "ab_cd" <= "ab_ce"
|
||||
c = "abc" in "ab cd"
|
||||
d = "" not in "hello"
|
||||
e = "--" is "--"
|
||||
f = "A" is "B"
|
||||
g = "--" is not "--"
|
||||
h = "A" is not "B"
|
||||
i = str_instance() < "..."
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
j = "ab" < "ab_cd"
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: bool
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
reveal_type(g) # revealed: bool
|
||||
reveal_type(h) # revealed: Literal[True]
|
||||
reveal_type(i) # revealed: bool
|
||||
reveal_type(j) # revealed: Literal[True]
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Unsupported operators
|
||||
|
||||
```py
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
c = object() < 5 # error: "Operator `<` is not supported for types `object` and `Literal[5]`"
|
||||
# TODO should error, need to check if __lt__ signature is valid for right operand
|
||||
d = 5 < object()
|
||||
|
||||
reveal_type(a) # revealed: bool
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: Unknown
|
||||
# TODO: should be `Unknown`
|
||||
reveal_type(d) # revealed: bool
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# If expressions
|
||||
|
||||
## Simple if-expression
|
||||
|
||||
```py
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## If-expression with walrus operator
|
||||
|
||||
```py
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
a = y
|
||||
b = z
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(a) # revealed: Literal[0, 1]
|
||||
reveal_type(b) # revealed: Literal[0, 2]
|
||||
```
|
||||
|
||||
## Nested if-expression
|
||||
|
||||
```py
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
# If statements
|
||||
|
||||
## Simple if
|
||||
|
||||
```py
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Simple if-elif-else
|
||||
|
||||
```py
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
else:
|
||||
r = y
|
||||
y = 5
|
||||
s = y
|
||||
x = y
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
reveal_type(r) # revealed: Unbound | Literal[2]
|
||||
reveal_type(s) # revealed: Unbound | Literal[5]
|
||||
```
|
||||
|
||||
## Single symbol across if-elif-else
|
||||
|
||||
```py
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## if-elif-else without else assignment
|
||||
|
||||
```py
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## if-elif-else with intervening assignment
|
||||
|
||||
```py
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
z = 3
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## Nested if statement
|
||||
|
||||
```py
|
||||
y = 0
|
||||
if flag:
|
||||
if flag2:
|
||||
y = 1
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## if-elif without else
|
||||
|
||||
```py
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# Pattern matching
|
||||
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Without wildcard
|
||||
|
||||
```py
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Unbound | Literal[2, 3]
|
||||
```
|
||||
|
||||
## Basic match
|
||||
|
||||
```py
|
||||
y = 1
|
||||
y = 2
|
||||
match 0:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# Errors while declaring
|
||||
|
||||
## Violates previous assignment
|
||||
|
||||
```py
|
||||
x = 1
|
||||
x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`"
|
||||
```
|
||||
|
||||
## Incompatible declarations
|
||||
|
||||
```py
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Partial declarations
|
||||
|
||||
```py
|
||||
if flag:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations with bad assignment
|
||||
|
||||
```py
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b'foo'
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
# Exception Handling
|
||||
|
||||
## Single Exception
|
||||
|
||||
```py
|
||||
import re
|
||||
try:
|
||||
x
|
||||
except NameError as e:
|
||||
reveal_type(e) # revealed: NameError
|
||||
except re.error as f:
|
||||
reveal_type(f) # revealed: error
|
||||
```
|
||||
|
||||
## Unknown type in except handler does not cause spurious diagnostic
|
||||
|
||||
```py
|
||||
from nonexistent_module import foo # error: [unresolved-import]
|
||||
|
||||
try:
|
||||
x
|
||||
except foo as e:
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Multiple Exceptions in a Tuple
|
||||
|
||||
```py
|
||||
EXCEPTIONS = (AttributeError, TypeError)
|
||||
|
||||
try:
|
||||
x
|
||||
except (RuntimeError, OSError) as e:
|
||||
reveal_type(e) # revealed: RuntimeError | OSError
|
||||
except EXCEPTIONS as f:
|
||||
reveal_type(f) # revealed: AttributeError | TypeError
|
||||
```
|
||||
|
||||
## Dynamic exception types
|
||||
|
||||
```py
|
||||
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
|
||||
try:
|
||||
w
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo
|
||||
except y as f:
|
||||
# TODO: should be `OSError | RuntimeError`
|
||||
reveal_type(f) # revealed: @Todo
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Except star
|
||||
|
||||
TODO(Alex): Once we support `sys.version_info` branches, we can set `--target-version=py311` in these tests and the inferred type will just be `BaseExceptionGroup`
|
||||
|
||||
## Except\* with BaseException
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
except* BaseException as e:
|
||||
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
|
||||
```
|
||||
|
||||
## Except\* with specific exception
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
except* OSError as e:
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
|
||||
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
|
||||
```
|
||||
|
||||
## Except\* with multiple exceptions
|
||||
|
||||
```py
|
||||
try:
|
||||
x
|
||||
except* (TypeError, AttributeError) as e:
|
||||
#TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
|
||||
reveal_type(e) # revealed: Unknown | BaseExceptionGroup
|
||||
```
|
||||
@@ -0,0 +1,151 @@
|
||||
# Expressions
|
||||
|
||||
## OR
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
a = True or False
|
||||
b = 'x' or 'y' or 'z'
|
||||
c = '' or 'y' or 'z'
|
||||
d = False or 'z'
|
||||
e = False or True
|
||||
f = False or False
|
||||
g = foo() or False
|
||||
h = foo() or True
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal["x"]
|
||||
reveal_type(c) # revealed: Literal["y"]
|
||||
reveal_type(d) # revealed: Literal["z"]
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
reveal_type(g) # revealed: str | Literal[False]
|
||||
reveal_type(h) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
a = True and False
|
||||
b = False and True
|
||||
c = foo() and False
|
||||
d = foo() and True
|
||||
e = 'x' and 'y' and 'z'
|
||||
f = 'x' and 'y' and ''
|
||||
g = '' and 'y'
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: str | Literal[False]
|
||||
reveal_type(d) # revealed: str | Literal[True]
|
||||
reveal_type(e) # revealed: Literal["z"]
|
||||
reveal_type(f) # revealed: Literal[""]
|
||||
reveal_type(g) # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Simple function calls to bool
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
x = True
|
||||
else:
|
||||
x = False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
a = "x" and "y" or "z"
|
||||
b = "x" or "y" and "z"
|
||||
c = "" and "y" or "z"
|
||||
d = "" or "y" and "z"
|
||||
e = "x" and "y" or ""
|
||||
f = "x" or "y" and ""
|
||||
|
||||
reveal_type(a) # revealed: Literal["y"]
|
||||
reveal_type(b) # revealed: Literal["x"]
|
||||
reveal_type(c) # revealed: Literal["z"]
|
||||
reveal_type(d) # revealed: Literal["z"]
|
||||
reveal_type(e) # revealed: Literal["y"]
|
||||
reveal_type(f) # revealed: Literal["x"]
|
||||
```
|
||||
|
||||
## `bool()` function
|
||||
|
||||
## Evaluates to builtin
|
||||
|
||||
```py path=a.py
|
||||
redefined_builtin_bool = bool
|
||||
|
||||
def my_bool(x)-> bool: pass
|
||||
```
|
||||
|
||||
```py
|
||||
from a import redefined_builtin_bool, my_bool
|
||||
a = redefined_builtin_bool(0)
|
||||
b = my_bool(0)
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: bool
|
||||
```
|
||||
|
||||
## Truthy values
|
||||
|
||||
```py
|
||||
a = bool(1)
|
||||
b = bool((0,))
|
||||
c = bool("NON EMPTY")
|
||||
d = bool(True)
|
||||
|
||||
def foo(): pass
|
||||
e = bool(foo)
|
||||
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[True]
|
||||
reveal_type(d) # revealed: Literal[True]
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Falsy values
|
||||
|
||||
```py
|
||||
a = bool(0)
|
||||
b = bool(())
|
||||
c = bool(None)
|
||||
d = bool("")
|
||||
e = bool(False)
|
||||
f = bool()
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: Literal[False]
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Ambiguous values
|
||||
|
||||
```py
|
||||
a = bool([])
|
||||
b = bool({})
|
||||
c = bool(set())
|
||||
|
||||
reveal_type(a) # revealed: bool
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: bool
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# Structures
|
||||
|
||||
## Class import following
|
||||
|
||||
```py
|
||||
from b import C as D; E = D
|
||||
reveal_type(E) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
```
|
||||
|
||||
## Module member resolution
|
||||
|
||||
```py
|
||||
import b; D = b.C
|
||||
reveal_type(D) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
# Importing builtin module
|
||||
|
||||
```py
|
||||
import builtins; x = builtins.copyright
|
||||
reveal_type(x) # revealed: Literal[copyright]
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
# Conditional imports
|
||||
|
||||
## Reimport
|
||||
|
||||
```py path=c.py
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
if flag:
|
||||
from c import f
|
||||
else:
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO we should not emit this error
|
||||
from b import f # error: [invalid-assignment] "Object of type `Literal[f, f]` is not assignable to `Literal[f, f]`"
|
||||
# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`.
|
||||
reveal_type(f) # revealed: Literal[f, f]
|
||||
```
|
||||
|
||||
## Reimport with stub declaration
|
||||
|
||||
When we have a declared type in one path and only an inferred-from-definition type in the other, we
|
||||
should still be able to unify those:
|
||||
|
||||
```py path=c.pyi
|
||||
x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
if flag:
|
||||
from c import x
|
||||
else:
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
from b import x
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Unresolved Imports
|
||||
|
||||
## Unresolved import statement
|
||||
|
||||
```py
|
||||
import bar # error: "Cannot resolve import `bar`"
|
||||
```
|
||||
|
||||
## Unresolved import from statement
|
||||
|
||||
```py
|
||||
from bar import baz # error: "Cannot resolve import `bar`"
|
||||
```
|
||||
|
||||
## Unresolved import from resolved module
|
||||
|
||||
```py path=a.py
|
||||
```
|
||||
|
||||
```py
|
||||
from a import thing # error: "Module `a` has no member `thing`"
|
||||
```
|
||||
|
||||
## Resolved import of symbol from unresolved import
|
||||
|
||||
```py path=a.py
|
||||
import foo as foo # error: "Cannot resolve import `foo`"
|
||||
```
|
||||
|
||||
Importing the unresolved import into a second file should not trigger an additional "unresolved
|
||||
import" violation:
|
||||
|
||||
```py
|
||||
from a import foo
|
||||
```
|
||||
|
||||
## No implicit shadowing error
|
||||
|
||||
```py path=b.py
|
||||
x: int
|
||||
```
|
||||
|
||||
```py
|
||||
from b import x
|
||||
|
||||
x = 'foo' # error: "Object of type `Literal["foo"]"
|
||||
```
|
||||
@@ -0,0 +1,133 @@
|
||||
# Relative
|
||||
|
||||
## Non-existent
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Simple
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import X
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Dotted
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo/bar/baz.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo.bar.baz import X
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Bare to package
|
||||
|
||||
```py path=package/__init__.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import X
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Non-existent + bare to package
|
||||
|
||||
```py path=package/bar.py
|
||||
from . import X # error: [unresolved-import]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
from .foo import X
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
## Non-existent + dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Long relative import
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/subpackage/subsubpackage/bar.py
|
||||
from ...foo import X
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## Unbound symbol
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
x
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
from .foo import x # error: [unresolved-import]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Bare to module
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
X = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
# TODO: support submodule imports
|
||||
from . import foo # error: [unresolved-import]
|
||||
y = foo.X
|
||||
|
||||
# TODO: should be `Literal[42]`
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Non-existent + bare to module
|
||||
|
||||
```py path=package/__init__.py
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
# TODO: submodule imports possibly not supported right now?
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Stubs
|
||||
|
||||
## Import from stub declaration
|
||||
|
||||
```py
|
||||
from b import x
|
||||
y = x
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
```py path=b.pyi
|
||||
x: int
|
||||
```
|
||||
|
||||
## Import from non-stub with declaration and definition
|
||||
|
||||
```py
|
||||
from b import x
|
||||
y = x
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
x: int = 1
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Boolean literals
|
||||
|
||||
```py
|
||||
x = True
|
||||
y = False
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: Literal[False]
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Dictionaries
|
||||
|
||||
## Empty dictionary
|
||||
|
||||
```py
|
||||
x = {}
|
||||
reveal_type(x) # revealed: dict
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Lists
|
||||
|
||||
## Empty list
|
||||
|
||||
```py
|
||||
x = []
|
||||
reveal_type(x) # revealed: list
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Sets
|
||||
|
||||
## Basic set
|
||||
|
||||
```py
|
||||
x = {1, 2}
|
||||
reveal_type(x) # revealed: set
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
# Tuples
|
||||
|
||||
## Empty tuple
|
||||
|
||||
```py
|
||||
x = ()
|
||||
reveal_type(x) # revealed: tuple[()]
|
||||
```
|
||||
|
||||
## Heterogeneous tuple
|
||||
|
||||
```py
|
||||
x = (1, 'a')
|
||||
y = (1, (2, 3))
|
||||
z = (x, 2)
|
||||
|
||||
reveal_type(x) # revealed: tuple[Literal[1], Literal["a"]]
|
||||
reveal_type(y) # revealed: tuple[Literal[1], tuple[Literal[2], Literal[3]]]
|
||||
reveal_type(z) # revealed: tuple[tuple[Literal[1], Literal["a"]], Literal[2]]
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Complex literals
|
||||
|
||||
## Complex numbers
|
||||
|
||||
```py
|
||||
reveal_type(2j) # revealed: complex
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# f-strings
|
||||
|
||||
## Expression
|
||||
|
||||
```py
|
||||
x = 0
|
||||
y = str()
|
||||
z = False
|
||||
|
||||
a = f'hello'
|
||||
b = f'h {x}'
|
||||
c = 'one ' f'single ' f'literal'
|
||||
d = 'first ' f'second({b})' f' third'
|
||||
e = f'-{y}-'
|
||||
f = f'-{y}-' f'--' '--'
|
||||
g = f'{z} == {False} is {True}'
|
||||
|
||||
reveal_type(a) # revealed: Literal["hello"]
|
||||
reveal_type(b) # revealed: Literal["h 0"]
|
||||
reveal_type(c) # revealed: Literal["one single literal"]
|
||||
reveal_type(d) # revealed: Literal["first second(h 0) third"]
|
||||
reveal_type(e) # revealed: str
|
||||
reveal_type(f) # revealed: str
|
||||
reveal_type(g) # revealed: Literal["False == False is True"]
|
||||
```
|
||||
|
||||
## Conversion Flags
|
||||
|
||||
```py
|
||||
string = 'hello'
|
||||
a = f'{string!r}'
|
||||
|
||||
# TODO: should be `Literal["'hello'"]`
|
||||
reveal_type(a) # revealed: str
|
||||
```
|
||||
|
||||
## Format Specifiers
|
||||
|
||||
```py
|
||||
a = f'{1:02}'
|
||||
|
||||
# TODO: should be `Literal["01"]`
|
||||
reveal_type(a) # revealed: str
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Float literals
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# Integer literals
|
||||
|
||||
## Literals
|
||||
|
||||
We can infer an integer literal type:
|
||||
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Variable
|
||||
|
||||
```py
|
||||
x = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Overflow
|
||||
|
||||
We only track integer literals within the range of an i64:
|
||||
|
||||
```py
|
||||
reveal_type(9223372036854775808) # revealed: int
|
||||
```
|
||||
|
||||
## Big int
|
||||
|
||||
We don't support big integer literals; we just infer `int` type instead:
|
||||
|
||||
```py
|
||||
x = 10_000_000_000_000_000_000
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Negated
|
||||
|
||||
```py
|
||||
x = -1
|
||||
y = -1234567890987654321
|
||||
z = --987
|
||||
reveal_type(x) # revealed: Literal[-1]
|
||||
reveal_type(y) # revealed: Literal[-1234567890987654321]
|
||||
reveal_type(z) # revealed: Literal[987]
|
||||
```
|
||||
|
||||
## Floats
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
```py
|
||||
reveal_type(2j) # revealed: complex
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# String literals
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
w = "Hello"
|
||||
x = 'world'
|
||||
y = "Guten " + 'tag'
|
||||
z = 'bon ' + "jour"
|
||||
|
||||
reveal_type(w) # revealed: Literal["Hello"]
|
||||
reveal_type(x) # revealed: Literal["world"]
|
||||
reveal_type(y) # revealed: Literal["Guten tag"]
|
||||
reveal_type(z) # revealed: Literal["bon jour"]
|
||||
```
|
||||
|
||||
## Nested Quotes
|
||||
|
||||
```py
|
||||
x = 'I say "hello" to you'
|
||||
y = "You say \"hey\" back"
|
||||
z = 'No "closure here'
|
||||
reveal_type(x) # revealed: Literal["I say \"hello\" to you"]
|
||||
reveal_type(y) # revealed: Literal["You say \"hey\" back"]
|
||||
reveal_type(z) # revealed: Literal["No \"closure here"]
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# Async
|
||||
|
||||
Async `for` loops do not work according to the synchronous iteration protocol.
|
||||
|
||||
## Invalid async for loop
|
||||
|
||||
```py
|
||||
async def foo():
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
async for x in Iterator():
|
||||
pass
|
||||
|
||||
# TODO
|
||||
reveal_type(x) # revealed: Unbound | @Todo
|
||||
```
|
||||
|
||||
## Basic async for loop
|
||||
|
||||
```py
|
||||
async def foo():
|
||||
class IntAsyncIterator:
|
||||
async def __anext__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntAsyncIterable:
|
||||
def __aiter__(self) -> IntAsyncIterator:
|
||||
return IntAsyncIterator()
|
||||
|
||||
#TODO(Alex): async iterables/iterators!
|
||||
async for x in IntAsyncIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | @Todo
|
||||
```
|
||||
@@ -0,0 +1,134 @@
|
||||
# For loops
|
||||
|
||||
## Basic `for` loop
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | int
|
||||
```
|
||||
|
||||
## With previous definition
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
x = 'foo'
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"] | int
|
||||
```
|
||||
|
||||
## With `else` (no break)
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
pass
|
||||
else:
|
||||
x = 'foo'
|
||||
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
## May `break`
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
for x in IntIterable():
|
||||
if x > 5:
|
||||
break
|
||||
else:
|
||||
x = 'foo'
|
||||
|
||||
reveal_type(x) # revealed: int | Literal["foo"]
|
||||
```
|
||||
|
||||
## With old-style iteration protocol
|
||||
|
||||
```py
|
||||
class OldStyleIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
for x in OldStyleIterable():
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | int
|
||||
```
|
||||
|
||||
## With heterogeneous tuple
|
||||
|
||||
```py
|
||||
for x in (1, 'a', b'foo'):
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
```
|
||||
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: Unbound | Unknown
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
pass
|
||||
```
|
||||
|
||||
## New over old style iteration protocol
|
||||
|
||||
```py
|
||||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# Iterators
|
||||
|
||||
## Yield must be iterable
|
||||
|
||||
```py
|
||||
class NotIterable: pass
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator: ...
|
||||
|
||||
def generator_function():
|
||||
yield from Iterable()
|
||||
yield from NotIterable() # error: "Object of type `NotIterable` is not iterable"
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# While loops
|
||||
|
||||
## Basic While Loop
|
||||
|
||||
```py
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## While with else (no break)
|
||||
|
||||
```py
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
else:
|
||||
y = x
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## While with Else (may break)
|
||||
|
||||
```py
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
x = 2
|
||||
if flag2:
|
||||
y = 4
|
||||
break
|
||||
else:
|
||||
y = x
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Narrowing for `is` conditionals
|
||||
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
|
||||
if x is None:
|
||||
# TODO the following should be simplified to 'None'
|
||||
reveal_type(x) # revealed: None | Literal[1] & None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
class A:
|
||||
...
|
||||
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
if y is x:
|
||||
# TODO the following should be simplified to 'A'
|
||||
reveal_type(y) # revealed: A | None & A
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Narrowing for `is not` conditionals
|
||||
|
||||
## `is not None`
|
||||
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
if x is not False:
|
||||
# TODO the following should be `Literal[True]`
|
||||
reveal_type(x) # revealed: bool & ~Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for non-singleton types
|
||||
|
||||
Non-singleton types should *not* narrow the type: two instances of a
|
||||
non-singleton class may occupy different addresses in memory even if
|
||||
they compare equal.
|
||||
|
||||
```py
|
||||
x = [1]
|
||||
y = [1]
|
||||
|
||||
if x is not y:
|
||||
# TODO: should include type parameter: list[int]
|
||||
reveal_type(x) # revealed: list
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# Narrowing for `match` statements
|
||||
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
y = 0
|
||||
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
# TODO intersection simplification: should be just Literal[0] | None
|
||||
reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# `is not None` narrowing
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# Classes shadowing
|
||||
|
||||
## Implicit error
|
||||
|
||||
```py
|
||||
class C: pass
|
||||
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
|
||||
```
|
||||
|
||||
## Explicit
|
||||
|
||||
No diagnostic is raised in the case of explicit shadowing:
|
||||
|
||||
```py
|
||||
class C: pass
|
||||
C: int = 1
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
# Function shadowing
|
||||
|
||||
## Parameter
|
||||
|
||||
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function. No diagnostics should be generated.
|
||||
|
||||
```py path=a.py
|
||||
def f(x: str):
|
||||
x: int = int(x)
|
||||
```
|
||||
|
||||
## Implicit error
|
||||
|
||||
```py path=a.py
|
||||
def f(): pass
|
||||
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
|
||||
```
|
||||
|
||||
## Explicit shadowing
|
||||
|
||||
```py path=a.py
|
||||
def f(): pass
|
||||
f: int = 1
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# Shadwing declaration
|
||||
|
||||
## Shadow after incompatible declarations is OK
|
||||
|
||||
```py
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x: bytes = b'foo'
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
# Class defenitions in stubs
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
```py path=a.pyi
|
||||
class C(C): ...
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Bytes subscript
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
w = b'red' b'knot'
|
||||
x = b'hello'
|
||||
y = b'world' + b'!'
|
||||
z = b'\xff\x00'
|
||||
|
||||
reveal_type(w) # revealed: Literal[b"redknot"]
|
||||
reveal_type(x) # revealed: Literal[b"hello"]
|
||||
reveal_type(y) # revealed: Literal[b"world!"]
|
||||
reveal_type(z) # revealed: Literal[b"\xff\x00"]
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# Class subscript
|
||||
|
||||
## Class getitem unbound
|
||||
|
||||
```py
|
||||
class NotSubscriptable: pass
|
||||
a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method"
|
||||
```
|
||||
|
||||
## Class getitem
|
||||
|
||||
```py
|
||||
class Identity:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
|
||||
a = Identity[0]
|
||||
reveal_type(a) # revealed: str
|
||||
```
|
||||
|
||||
## Class getitem union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
|
||||
class Identity:
|
||||
if flag:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
else:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
a = Identity[0]
|
||||
reveal_type(a) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with class union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
|
||||
class Identity1:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
|
||||
class Identity2:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
if flag:
|
||||
a = Identity1
|
||||
else:
|
||||
a = Identity2
|
||||
|
||||
b = a[0]
|
||||
reveal_type(a) # revealed: Literal[Identity1, Identity2]
|
||||
reveal_type(b) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with unbound method union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
|
||||
if flag:
|
||||
class Identity:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
pass
|
||||
else:
|
||||
class Identity: pass
|
||||
|
||||
a = Identity[42] # error: [call-non-callable] "Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Identity, Identity]`"
|
||||
reveal_type(a) # revealed: str | Unknown
|
||||
```
|
||||
|
||||
## TODO: Class getitem non-class union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
|
||||
if flag:
|
||||
class Identity:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
pass
|
||||
else:
|
||||
Identity = 1
|
||||
|
||||
a = Identity[42] # error: "Cannot subscript object of type `Literal[Identity] | Literal[1]` with no `__getitem__` method"
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# Instance subscript
|
||||
|
||||
## Getitem unbound
|
||||
|
||||
```py
|
||||
class NotSubscriptable: pass
|
||||
a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscriptable` with no `__getitem__` method"
|
||||
```
|
||||
|
||||
## Getitem not callable
|
||||
|
||||
```py
|
||||
class NotSubscriptable:
|
||||
__getitem__ = None
|
||||
|
||||
a = NotSubscriptable()[0] # error: "Method `__getitem__` of type `None` is not callable on object of type `NotSubscriptable`"
|
||||
```
|
||||
|
||||
## Valid getitem
|
||||
|
||||
```py
|
||||
class Identity:
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
|
||||
a = Identity()[0]
|
||||
reveal_type(a) # revealed: int
|
||||
```
|
||||
|
||||
## Getitem union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
|
||||
class Identity:
|
||||
if flag:
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
else:
|
||||
def __getitem__(self, index: int) -> str:
|
||||
return str(index)
|
||||
|
||||
a = Identity()[0]
|
||||
reveal_type(a) # revealed: int | str
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Subscript on strings
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
s = 'abcde'
|
||||
|
||||
a = s[0]
|
||||
b = s[1]
|
||||
c = s[-1]
|
||||
d = s[-2]
|
||||
e = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
f = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
|
||||
reveal_type(a) # revealed: Literal["a"]
|
||||
reveal_type(b) # revealed: Literal["b"]
|
||||
reveal_type(c) # revealed: Literal["e"]
|
||||
reveal_type(d) # revealed: Literal["d"]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function return
|
||||
|
||||
```py
|
||||
def add(x: int, y: int) -> int:
|
||||
return x + y
|
||||
|
||||
a = 'abcde'[add(0, 1)]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Tuple subscripts
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
t = (1, 'a', 'b')
|
||||
|
||||
a = t[0]
|
||||
b = t[1]
|
||||
c = t[-1]
|
||||
d = t[-2]
|
||||
e = t[4] # error: [index-out-of-bounds]
|
||||
f = t[-4] # error: [index-out-of-bounds]
|
||||
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal["a"]
|
||||
reveal_type(c) # revealed: Literal["b"]
|
||||
reveal_type(d) # revealed: Literal["a"]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
# Unary Operations
|
||||
|
||||
## Unary Addition
|
||||
|
||||
```py
|
||||
a = +0
|
||||
b = +1
|
||||
c = +True
|
||||
|
||||
reveal_type(a) # revealed: Literal[0]
|
||||
reveal_type(b) # revealed: Literal[1]
|
||||
reveal_type(c) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Unary Subtraction
|
||||
|
||||
```py
|
||||
a = -0
|
||||
b = -1
|
||||
c = -True
|
||||
|
||||
reveal_type(a) # revealed: Literal[0]
|
||||
reveal_type(b) # revealed: Literal[-1]
|
||||
reveal_type(c) # revealed: Literal[-1]
|
||||
```
|
||||
|
||||
## Unary Bitwise Inversion
|
||||
|
||||
```py
|
||||
a = ~0
|
||||
b = ~1
|
||||
c = ~True
|
||||
|
||||
reveal_type(a) # revealed: Literal[-1]
|
||||
reveal_type(b) # revealed: Literal[-2]
|
||||
reveal_type(c) # revealed: Literal[-2]
|
||||
```
|
||||
149
crates/red_knot_python_semantic/resources/mdtest/unary/not.md
Normal file
149
crates/red_knot_python_semantic/resources/mdtest/unary/not.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Unary not
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
a = not None
|
||||
b = not not None
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Function
|
||||
|
||||
```py
|
||||
from typing import reveal_type
|
||||
|
||||
def f():
|
||||
return 1
|
||||
|
||||
a = not f
|
||||
b = not reveal_type
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
# TODO Unknown should not be part of the type of typing.reveal_type
|
||||
# reveal_type(b) revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Module
|
||||
|
||||
```py
|
||||
import b; import warnings
|
||||
|
||||
x = not b
|
||||
z = not warnings
|
||||
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
y = 1
|
||||
```
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
if flag:
|
||||
p = 1
|
||||
q = 3.3
|
||||
r = "hello"
|
||||
s = "world"
|
||||
t = 0
|
||||
else:
|
||||
p = "hello"
|
||||
q = 4
|
||||
r = ""
|
||||
s = 0
|
||||
t = ""
|
||||
|
||||
a = not p
|
||||
b = not q
|
||||
c = not r
|
||||
d = not s
|
||||
e = not t
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: bool
|
||||
reveal_type(c) # revealed: bool
|
||||
reveal_type(d) # revealed: bool
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Integer literal
|
||||
|
||||
```py
|
||||
a = not 1
|
||||
b = not 1234567890987654321
|
||||
e = not 0
|
||||
x = not -1
|
||||
y = not -1234567890987654321
|
||||
z = not --987
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(e) # revealed: Literal[True]
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: Literal[False]
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Boolean literal
|
||||
|
||||
```py
|
||||
w = True
|
||||
x = False
|
||||
y = not w
|
||||
z = not x
|
||||
|
||||
reveal_type(w) # revealed: Literal[True]
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: Literal[False]
|
||||
reveal_type(z) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## String literal
|
||||
|
||||
```py
|
||||
a = not "hello"
|
||||
b = not ""
|
||||
c = not "0"
|
||||
d = not "hello" + "world"
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Bytes literal
|
||||
|
||||
```py
|
||||
a = not b"hello"
|
||||
b = not b""
|
||||
c = not b"0"
|
||||
d = not b"hello" + b"world"
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[True]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Tuple
|
||||
|
||||
```py
|
||||
a = not (1,)
|
||||
b = not (1, 2)
|
||||
c = not (1, 2, 3)
|
||||
d = not ()
|
||||
e = not ("hello",)
|
||||
f = not (1, "hello")
|
||||
|
||||
reveal_type(a) # revealed: Literal[False]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[True]
|
||||
reveal_type(e) # revealed: Literal[False]
|
||||
reveal_type(f) # revealed: Literal[False]
|
||||
```
|
||||
273
crates/red_knot_python_semantic/resources/mdtest/unpacking.md
Normal file
273
crates/red_knot_python_semantic/resources/mdtest/unpacking.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Unpacking
|
||||
|
||||
## Tuple
|
||||
|
||||
### Simple tuple
|
||||
|
||||
```py
|
||||
(a, b, c) = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
### Simple list
|
||||
|
||||
```py
|
||||
[a, b, c] = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
### Simple mixed
|
||||
|
||||
```py
|
||||
[a, (b, c), d] = (1, (2, 3), 4)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
reveal_type(d) # revealed: Literal[4]
|
||||
```
|
||||
|
||||
### Multiple assignment
|
||||
|
||||
```py
|
||||
a, b = c = 1, 2
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(c) # revealed: tuple[Literal[1], Literal[2]]
|
||||
```
|
||||
|
||||
### Nested tuple with unpacking
|
||||
|
||||
```py
|
||||
(a, (b, c), d) = (1, (2, 3), 4)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
reveal_type(d) # revealed: Literal[4]
|
||||
```
|
||||
|
||||
### Nested tuple without unpacking
|
||||
|
||||
```py
|
||||
(a, b, c) = (1, (2, 3), 4)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: tuple[Literal[2], Literal[3]]
|
||||
reveal_type(c) # revealed: Literal[4]
|
||||
```
|
||||
|
||||
### Uneven unpacking (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (there aren't enough values to unpack)
|
||||
(a, b, c) = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Uneven unpacking (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (too many values to unpack)
|
||||
(a, b) = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Starred expression (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, *b, c, d] = (1, 2) # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Starred expression (2)
|
||||
|
||||
```py
|
||||
[a, *b, c] = (1, 2) # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Starred expression (3)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, *b, c] = (1, 2, 3) # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
### Starred expression (4)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, *b, c, d] = (1, 2, 3, 4, 5, 6) # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: Literal[5]
|
||||
reveal_type(d) # revealed: Literal[6]
|
||||
```
|
||||
|
||||
### Starred expression (5)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
[a, b, *c] = (1, 2, 3, 4) # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo
|
||||
```
|
||||
|
||||
### Non-iterable unpacking
|
||||
|
||||
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like
|
||||
assignment target, multiple definitions are created and the inference engine runs
|
||||
on each of them which results in duplicate diagnostics.
|
||||
|
||||
```py
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
a, b = 1
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Custom iterator unpacking
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
|
||||
(a, b) = Iterable()
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
### Custom iterator unpacking nested
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
|
||||
(a, (b, c), d) = (1, Iterable(), 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: int
|
||||
reveal_type(c) # revealed: int
|
||||
reveal_type(d) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## String
|
||||
|
||||
### Simple unpacking
|
||||
|
||||
```py
|
||||
a, b = 'ab'
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Uneven unpacking (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (there aren't enough values to unpack)
|
||||
a, b, c = 'ab'
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
reveal_type(c) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Uneven unpacking (2)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (too many values to unpack)
|
||||
a, b = 'abc'
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Starred expression (1)
|
||||
|
||||
```py
|
||||
# TODO: Add diagnostic (need more values to unpack)
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, *b, c, d) = "ab" # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Starred expression (2)
|
||||
|
||||
```py
|
||||
(a, *b, c) = "ab" # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Starred expression (3)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, *b, c) = "abc" # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Starred expression (4)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, *b, c, d) = "abcdef" # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Starred expression (5)
|
||||
|
||||
```py
|
||||
# TODO: Remove 'not-iterable' diagnostic
|
||||
(a, b, *c) = "abcd" # error: "Object of type `None` is not iterable"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo
|
||||
```
|
||||
@@ -17,7 +17,7 @@ use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
|
||||
pub fn resolve_module(db: &dyn Db, module_name: &ModuleName) -> Option<Module> {
|
||||
let interned_name = ModuleNameIngredient::new(db, module_name);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
@@ -103,7 +103,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
// If it doesn't, then that means that multiple modules have the same name in different
|
||||
// root paths, but that the module corresponding to `path` is in a lower priority search path,
|
||||
// in which case we ignore it.
|
||||
let module = resolve_module(db, module_name)?;
|
||||
let module = resolve_module(db, &module_name)?;
|
||||
|
||||
if file == module.file() {
|
||||
Some(module)
|
||||
@@ -728,11 +728,11 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&foo_module),
|
||||
resolve_module(&db, foo_module_name.clone()).as_ref()
|
||||
resolve_module(&db, &foo_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!("foo", foo_module.name());
|
||||
@@ -755,7 +755,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
let builtins = resolve_module(&db, builtins_module_name).expect("builtins to resolve");
|
||||
let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve");
|
||||
|
||||
assert_eq!(builtins.file().path(&db), &stdlib.join("builtins.pyi"));
|
||||
}
|
||||
@@ -776,7 +776,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
let builtins = resolve_module(&db, builtins_module_name).expect("builtins to resolve");
|
||||
let builtins = resolve_module(&db, &builtins_module_name).expect("builtins to resolve");
|
||||
|
||||
assert_eq!(builtins.file().path(&db), &stdlib.join("builtins.pyi"));
|
||||
}
|
||||
@@ -794,11 +794,11 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&functools_module),
|
||||
resolve_module(&db, functools_module_name).as_ref()
|
||||
resolve_module(&db, &functools_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!(&stdlib, functools_module.search_path());
|
||||
@@ -848,7 +848,7 @@ mod tests {
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
for module_name in existing_modules {
|
||||
let resolved_module = resolve_module(&db, module_name.clone()).unwrap_or_else(|| {
|
||||
let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let search_path = resolved_module.search_path();
|
||||
@@ -901,7 +901,7 @@ mod tests {
|
||||
|
||||
for module_name in nonexisting_modules {
|
||||
assert!(
|
||||
resolve_module(&db, module_name.clone()).is_none(),
|
||||
resolve_module(&db, &module_name).is_none(),
|
||||
"Unexpectedly resolved a module for {module_name}"
|
||||
);
|
||||
}
|
||||
@@ -944,7 +944,7 @@ mod tests {
|
||||
]);
|
||||
|
||||
for module_name in existing_modules {
|
||||
let resolved_module = resolve_module(&db, module_name.clone()).unwrap_or_else(|| {
|
||||
let resolved_module = resolve_module(&db, &module_name).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let search_path = resolved_module.search_path();
|
||||
@@ -980,7 +980,7 @@ mod tests {
|
||||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
for module_name in nonexisting_modules {
|
||||
assert!(
|
||||
resolve_module(&db, module_name.clone()).is_none(),
|
||||
resolve_module(&db, &module_name).is_none(),
|
||||
"Unexpectedly resolved a module for {module_name}"
|
||||
);
|
||||
}
|
||||
@@ -1002,11 +1002,11 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(&functools_module),
|
||||
resolve_module(&db, functools_module_name).as_ref()
|
||||
resolve_module(&db, &functools_module_name).as_ref()
|
||||
);
|
||||
assert_eq!(&src, functools_module.search_path());
|
||||
assert_eq!(ModuleKind::Module, functools_module.kind());
|
||||
@@ -1026,7 +1026,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
let pydoc_data_topics = resolve_module(&db, pydoc_data_topics_name).unwrap();
|
||||
let pydoc_data_topics = resolve_module(&db, &pydoc_data_topics_name).unwrap();
|
||||
|
||||
assert_eq!("pydoc_data.topics", pydoc_data_topics.name());
|
||||
assert_eq!(pydoc_data_topics.search_path(), &stdlib);
|
||||
@@ -1043,7 +1043,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let foo_path = src.join("foo/__init__.py");
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
|
||||
assert_eq!("foo", foo_module.name());
|
||||
assert_eq!(&src, foo_module.search_path());
|
||||
@@ -1070,7 +1070,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
assert_eq!(&src, foo_module.search_path());
|
||||
@@ -1098,11 +1098,11 @@ mod tests {
|
||||
let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
|
||||
|
||||
// `foo.py` takes priority over the `foo` namespace package
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
assert_eq!(foo_module.file().path(&db), &src.join("foo.py"));
|
||||
|
||||
// `foo.bar` isn't recognised as a module
|
||||
let foo_bar_module = resolve_module(&db, foo_bar_module_name.clone());
|
||||
let foo_bar_module = resolve_module(&db, &foo_bar_module_name);
|
||||
assert_eq!(foo_bar_module, None);
|
||||
}
|
||||
|
||||
@@ -1112,7 +1112,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_stub = src.join("foo.pyi");
|
||||
|
||||
assert_eq!(&src, foo.search_path());
|
||||
@@ -1136,7 +1136,7 @@ mod tests {
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let baz_module =
|
||||
resolve_module(&db, ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
|
||||
resolve_module(&db, &ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
|
||||
let baz_path = src.join("foo/bar/baz.py");
|
||||
|
||||
assert_eq!(&src, baz_module.search_path());
|
||||
@@ -1175,14 +1175,14 @@ mod tests {
|
||||
let one_module_name = ModuleName::new_static("parent.child.one").unwrap();
|
||||
let one_module_path = FilePath::System(src.join("parent/child/one.py"));
|
||||
assert_eq!(
|
||||
resolve_module(&db, one_module_name),
|
||||
resolve_module(&db, &one_module_name),
|
||||
path_to_module(&db, &one_module_path)
|
||||
);
|
||||
|
||||
let two_module_name = ModuleName::new_static("parent.child.two").unwrap();
|
||||
let two_module_path = FilePath::System(site_packages.join("parent/child/two.py"));
|
||||
assert_eq!(
|
||||
resolve_module(&db, two_module_name),
|
||||
resolve_module(&db, &two_module_name),
|
||||
path_to_module(&db, &two_module_path)
|
||||
);
|
||||
}
|
||||
@@ -1215,12 +1215,12 @@ mod tests {
|
||||
|
||||
let one_module_path = FilePath::System(src.join("parent/child/one.py"));
|
||||
let one_module_name =
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap());
|
||||
resolve_module(&db, &ModuleName::new_static("parent.child.one").unwrap());
|
||||
assert_eq!(one_module_name, path_to_module(&db, &one_module_path));
|
||||
|
||||
assert_eq!(
|
||||
None,
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.two").unwrap())
|
||||
resolve_module(&db, &ModuleName::new_static("parent.child.two").unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1236,7 +1236,7 @@ mod tests {
|
||||
.with_site_packages_files(&[("foo.py", "")])
|
||||
.build();
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_src_path = src.join("foo.py");
|
||||
|
||||
assert_eq!(&src, foo_module.search_path());
|
||||
@@ -1301,8 +1301,8 @@ mod tests {
|
||||
)
|
||||
.context("Invalid program settings")?;
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module = resolve_module(&db, ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
let foo_module = resolve_module(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module = resolve_module(&db, &ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
|
||||
assert_ne!(foo_module, bar_module);
|
||||
|
||||
@@ -1337,7 +1337,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
|
||||
let bar_path = src.join("bar.py");
|
||||
let bar = system_path_to_file(&db, &bar_path).expect("bar.py to exist");
|
||||
@@ -1351,7 +1351,7 @@ mod tests {
|
||||
// Re-query the foo module. The foo module should still be cached because `bar.py` isn't relevant
|
||||
// for resolving `foo`.
|
||||
|
||||
let foo_module2 = resolve_module(&db, foo_module_name);
|
||||
let foo_module2 = resolve_module(&db, &foo_module_name);
|
||||
|
||||
assert!(!db
|
||||
.take_salsa_events()
|
||||
@@ -1368,14 +1368,14 @@ mod tests {
|
||||
let foo_path = src.join("foo.py");
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
assert_eq!(resolve_module(&db, foo_module_name.clone()), None);
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
|
||||
// Now write the foo file
|
||||
db.write_file(&foo_path, "x = 1")?;
|
||||
|
||||
let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist");
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve");
|
||||
let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve");
|
||||
assert_eq!(foo_file, foo_module.file());
|
||||
|
||||
Ok(())
|
||||
@@ -1389,7 +1389,7 @@ mod tests {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).expect("foo module to exist");
|
||||
let foo_module = resolve_module(&db, &foo_module_name).expect("foo module to exist");
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
assert_eq!(&foo_init_path, foo_module.file().path(&db));
|
||||
@@ -1401,7 +1401,7 @@ mod tests {
|
||||
File::sync_path(&mut db, &foo_init_path);
|
||||
File::sync_path(&mut db, foo_init_path.parent().unwrap());
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve");
|
||||
let foo_module = resolve_module(&db, &foo_module_name).expect("Foo module to resolve");
|
||||
assert_eq!(&src.join("foo.py"), foo_module.file().path(&db));
|
||||
|
||||
Ok(())
|
||||
@@ -1427,7 +1427,7 @@ mod tests {
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let stdlib_functools_path = stdlib.join("functools.pyi");
|
||||
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file()),
|
||||
@@ -1440,7 +1440,7 @@ mod tests {
|
||||
let site_packages_functools_path = site_packages.join("functools.py");
|
||||
db.write_file(&site_packages_functools_path, "f: int")
|
||||
.unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
let events = db.take_salsa_events();
|
||||
assert_function_query_was_not_run(
|
||||
&db,
|
||||
@@ -1473,7 +1473,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file()),
|
||||
@@ -1484,7 +1484,7 @@ mod tests {
|
||||
// since first-party files take higher priority in module resolution:
|
||||
let src_functools_path = src.join("functools.py");
|
||||
db.write_file(&src_functools_path, "FOO: int").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &src);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file()),
|
||||
@@ -1515,7 +1515,7 @@ mod tests {
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let src_functools_path = src.join("functools.py");
|
||||
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &src);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file()),
|
||||
@@ -1528,7 +1528,7 @@ mod tests {
|
||||
.remove_file(&src_functools_path)
|
||||
.unwrap();
|
||||
File::sync_path(&mut db, &src_functools_path);
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let functools_module = resolve_module(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(), &stdlib);
|
||||
assert_eq!(
|
||||
Ok(functools_module.file()),
|
||||
@@ -1550,8 +1550,8 @@ mod tests {
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_bar_module = resolve_module(&db, foo_bar_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let foo_bar_module = resolve_module(&db, &foo_bar_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo_module.file().path(&db),
|
||||
@@ -1579,11 +1579,11 @@ mod tests {
|
||||
|
||||
// Lines with leading whitespace in `.pth` files do not parse:
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
assert_eq!(resolve_module(&db, foo_module_name), None);
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
|
||||
// Lines with trailing whitespace in `.pth` files do:
|
||||
let bar_module_name = ModuleName::new_static("bar").unwrap();
|
||||
let bar_module = resolve_module(&db, bar_module_name.clone()).unwrap();
|
||||
let bar_module = resolve_module(&db, &bar_module_name).unwrap();
|
||||
assert_eq!(
|
||||
bar_module.file().path(&db),
|
||||
&FilePath::system("/y/src/bar.py")
|
||||
@@ -1602,7 +1602,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo_module.file().path(&db),
|
||||
@@ -1650,10 +1650,10 @@ not_a_directory
|
||||
let b_module_name = ModuleName::new_static("b").unwrap();
|
||||
let spam_module_name = ModuleName::new_static("spam").unwrap();
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let a_module = resolve_module(&db, a_module_name.clone()).unwrap();
|
||||
let b_module = resolve_module(&db, b_module_name.clone()).unwrap();
|
||||
let spam_module = resolve_module(&db, spam_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let a_module = resolve_module(&db, &a_module_name).unwrap();
|
||||
let b_module = resolve_module(&db, &b_module_name).unwrap();
|
||||
let spam_module = resolve_module(&db, &spam_module_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo_module.file().path(&db),
|
||||
@@ -1681,14 +1681,14 @@ not_a_directory
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let bar_module_name = ModuleName::new_static("bar").unwrap();
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
assert_eq!(
|
||||
foo_module.file().path(&db),
|
||||
&FilePath::system("/x/src/foo.py")
|
||||
);
|
||||
|
||||
db.clear_salsa_events();
|
||||
let bar_module = resolve_module(&db, bar_module_name).unwrap();
|
||||
let bar_module = resolve_module(&db, &bar_module_name).unwrap();
|
||||
assert_eq!(
|
||||
bar_module.file().path(&db),
|
||||
&FilePath::system("/y/src/bar.py")
|
||||
@@ -1713,7 +1713,7 @@ not_a_directory
|
||||
db.write_files(x_directory).unwrap();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
assert_eq!(
|
||||
foo_module.file().path(&db),
|
||||
&FilePath::system("/x/src/foo.py")
|
||||
@@ -1725,7 +1725,7 @@ not_a_directory
|
||||
|
||||
File::sync_path(&mut db, &site_packages.join("_foo.pth"));
|
||||
|
||||
assert_eq!(resolve_module(&db, foo_module_name.clone()), None);
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1740,7 +1740,7 @@ not_a_directory
|
||||
db.write_files(x_directory).unwrap();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
let foo_module = resolve_module(&db, &foo_module_name).unwrap();
|
||||
let src_path = SystemPathBuf::from("/x/src");
|
||||
assert_eq!(
|
||||
foo_module.file().path(&db),
|
||||
@@ -1753,7 +1753,7 @@ not_a_directory
|
||||
db.memory_file_system().remove_directory(&src_path).unwrap();
|
||||
File::sync_path(&mut db, &src_path.join("foo.py"));
|
||||
File::sync_path(&mut db, &src_path);
|
||||
assert_eq!(resolve_module(&db, foo_module_name.clone()), None);
|
||||
assert_eq!(resolve_module(&db, &foo_module_name), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1812,7 +1812,7 @@ not_a_directory
|
||||
// The editable installs discovered from the `.pth` file in the first `site-packages` directory
|
||||
// take precedence over the second `site-packages` directory...
|
||||
let a_module_name = ModuleName::new_static("a").unwrap();
|
||||
let a_module = resolve_module(&db, a_module_name.clone()).unwrap();
|
||||
let a_module = resolve_module(&db, &a_module_name).unwrap();
|
||||
assert_eq!(a_module.file().path(&db), &editable_install_location);
|
||||
|
||||
db.memory_file_system()
|
||||
@@ -1823,7 +1823,7 @@ not_a_directory
|
||||
// ...But now that the `.pth` file in the first `site-packages` directory has been deleted,
|
||||
// the editable install no longer exists, so the module now resolves to the file in the
|
||||
// second `site-packages` directory
|
||||
let a_module = resolve_module(&db, a_module_name).unwrap();
|
||||
let a_module = resolve_module(&db, &a_module_name).unwrap();
|
||||
assert_eq!(a_module.file().path(&db), &system_site_packages_location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
@@ -31,7 +31,7 @@ pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
|
||||
/// Returns the semantic index for `file`.
|
||||
///
|
||||
@@ -994,7 +994,7 @@ class C[T]:
|
||||
let ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(num),
|
||||
..
|
||||
}) = &*assignment.assignment().value
|
||||
}) = assignment.value()
|
||||
else {
|
||||
panic!("should be a number literal")
|
||||
};
|
||||
|
||||
@@ -28,8 +28,8 @@ use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, PatternConstraint};
|
||||
use super::definition::{
|
||||
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||
WithItemDefinitionNodeRef,
|
||||
AssignmentKind, DefinitionCategory, ExceptHandlerDefinitionNodeRef,
|
||||
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
};
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
@@ -566,11 +566,22 @@ where
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.visit_expr(&node.value);
|
||||
self.add_standalone_expression(&node.value);
|
||||
self.current_assignment = Some(node.into());
|
||||
for target in &node.targets {
|
||||
for (target_index, target) in node.targets.iter().enumerate() {
|
||||
let kind = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(AssignmentKind::Sequence),
|
||||
ast::Expr::Name(_) => Some(AssignmentKind::Name),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(kind) = kind {
|
||||
self.current_assignment = Some(CurrentAssignment::Assign {
|
||||
assignment: node,
|
||||
target_index,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
self.visit_expr(target);
|
||||
self.current_assignment = None;
|
||||
}
|
||||
self.current_assignment = None;
|
||||
}
|
||||
ast::Stmt::AnnAssign(node) => {
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
@@ -815,12 +826,18 @@ where
|
||||
let symbol = self.add_symbol(id.clone());
|
||||
if is_definition {
|
||||
match self.current_assignment {
|
||||
Some(CurrentAssignment::Assign(assignment)) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
assignment,
|
||||
target_index,
|
||||
kind,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
AssignmentDefinitionNodeRef {
|
||||
assignment,
|
||||
target: name_node,
|
||||
target_index,
|
||||
name: name_node,
|
||||
kind,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1045,7 +1062,11 @@ where
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign(&'a ast::StmtAssign),
|
||||
Assign {
|
||||
assignment: &'a ast::StmtAssign,
|
||||
target_index: usize,
|
||||
kind: AssignmentKind,
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
For(&'a ast::StmtFor),
|
||||
@@ -1057,12 +1078,6 @@ enum CurrentAssignment<'a> {
|
||||
WithItem(&'a ast::WithItem),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAssign> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtAssign) -> Self {
|
||||
Self::Assign(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtAnnAssign) -> Self {
|
||||
Self::AnnAssign(value)
|
||||
|
||||
@@ -161,7 +161,9 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) assignment: &'a ast::StmtAssign,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) target_index: usize,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) kind: AssignmentKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -224,12 +226,17 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionNodeRef::NamedExpression(named) => {
|
||||
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
|
||||
}
|
||||
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef { assignment, target }) => {
|
||||
DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef::new(parsed.clone(), assignment),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
|
||||
assignment,
|
||||
target_index,
|
||||
name,
|
||||
kind,
|
||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef::new(parsed.clone(), assignment),
|
||||
target_index,
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
kind,
|
||||
}),
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
}
|
||||
@@ -300,8 +307,10 @@ impl DefinitionNodeRef<'_> {
|
||||
Self::NamedExpression(node) => node.into(),
|
||||
Self::Assignment(AssignmentDefinitionNodeRef {
|
||||
assignment: _,
|
||||
target,
|
||||
}) => target.into(),
|
||||
target_index: _,
|
||||
name,
|
||||
kind: _,
|
||||
}) => name.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
Self::For(ForStmtDefinitionNodeRef {
|
||||
@@ -485,17 +494,34 @@ impl ImportFromDefinitionKind {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef<ast::StmtAssign>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
target_index: usize,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
kind: AssignmentKind,
|
||||
}
|
||||
|
||||
impl AssignmentDefinitionKind {
|
||||
pub(crate) fn assignment(&self) -> &ast::StmtAssign {
|
||||
self.assignment.node()
|
||||
pub(crate) fn value(&self) -> &ast::Expr {
|
||||
&self.assignment.node().value
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
pub(crate) fn target(&self) -> &ast::Expr {
|
||||
&self.assignment.node().targets[self.target_index]
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> AssignmentKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of assignment target expression.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AssignmentKind {
|
||||
Sequence,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -35,7 +35,7 @@ impl<'db> SemanticModel<'db> {
|
||||
line_index(self.db.upcast(), self.file)
|
||||
}
|
||||
|
||||
pub fn resolve_module(&self, module_name: ModuleName) -> Option<Module> {
|
||||
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module> {
|
||||
resolve_module(self.db, module_name)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ fn core_module_symbol_ty<'db>(
|
||||
core_module: CoreStdlibModule,
|
||||
symbol: &str,
|
||||
) -> Type<'db> {
|
||||
resolve_module(db, core_module.name())
|
||||
resolve_module(db, &core_module.name())
|
||||
.map(|module| global_symbol_ty(db, module.file(), symbol))
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ pub(crate) fn typing_extensions_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) ->
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<ScopeId<'_>> {
|
||||
resolve_module(db, core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
resolve_module(db, &core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
}
|
||||
|
||||
/// Get the `builtins` module scope.
|
||||
|
||||
@@ -14,10 +14,10 @@ use crate::stdlib::{
|
||||
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
|
||||
};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet};
|
||||
use crate::{Db, FxOrderSet, Module};
|
||||
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub(crate) use self::diagnostic::TypeCheckDiagnostics;
|
||||
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
pub(crate) use self::display::TypeArrayDisplay;
|
||||
pub(crate) use self::infer::{
|
||||
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
|
||||
@@ -385,14 +385,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builtin_str_instance(db: &'db dyn Db) -> Self {
|
||||
builtins_symbol_ty(db, "str").to_instance(db)
|
||||
}
|
||||
|
||||
pub fn builtin_int_instance(db: &'db dyn Db) -> Self {
|
||||
builtins_symbol_ty(db, "int").to_instance(db)
|
||||
}
|
||||
|
||||
pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
match self {
|
||||
Type::Class(class) => class.is_stdlib_symbol(db, module_name, name),
|
||||
@@ -423,19 +415,17 @@ impl<'db> Type<'db> {
|
||||
(_, Type::Unknown | Type::Any | Type::Todo) => false,
|
||||
(Type::Never, _) => true,
|
||||
(_, Type::Never) => false,
|
||||
(Type::IntLiteral(_), Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "int") =>
|
||||
{
|
||||
(Type::IntLiteral(_), Type::Instance(class)) if class.is_known(db, KnownClass::Int) => {
|
||||
true
|
||||
}
|
||||
(Type::StringLiteral(_), Type::LiteralString) => true,
|
||||
(Type::StringLiteral(_) | Type::LiteralString, Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "str") =>
|
||||
if class.is_known(db, KnownClass::Str) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::BytesLiteral(_), Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "bytes") =>
|
||||
if class.is_known(db, KnownClass::Bytes) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
@@ -443,8 +433,8 @@ impl<'db> Type<'db> {
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_subtype_of(db, elem_ty)),
|
||||
(_, Type::Instance(class)) if class.is_stdlib_symbol(db, "builtins", "object") => true,
|
||||
(Type::Instance(class), _) if class.is_stdlib_symbol(db, "builtins", "object") => false,
|
||||
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
|
||||
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
|
||||
// TODO
|
||||
_ => false,
|
||||
}
|
||||
@@ -473,6 +463,57 @@ impl<'db> Type<'db> {
|
||||
self == other
|
||||
}
|
||||
|
||||
/// Return true if there is just a single inhabitant for this type.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return `false`
|
||||
/// for more complicated types that are actually singletons.
|
||||
pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Any
|
||||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Todo
|
||||
| Type::Unbound
|
||||
| Type::Instance(..) // TODO some instance types can be singleton types (EllipsisType, NotImplementedType)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::LiteralString => {
|
||||
// Note: The literal types included in this pattern are not true singletons.
|
||||
// There can be multiple Python objects (at different memory locations) that
|
||||
// are both of type Literal[345], for example.
|
||||
false
|
||||
}
|
||||
Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true,
|
||||
Type::Tuple(tuple) => {
|
||||
// We deliberately deviate from the language specification [1] here and claim
|
||||
// that the empty tuple type is a singleton type. The reasoning is that `()`
|
||||
// is often used as a sentinel value in user code. Declaring the empty tuple to
|
||||
// be of singleton type allows us to narrow types in `is not ()` conditionals.
|
||||
//
|
||||
// [1] https://docs.python.org/3/reference/expressions.html#parenthesized-forms
|
||||
tuple.elements(db).is_empty()
|
||||
}
|
||||
Type::Union(..) => {
|
||||
// A single-element union, where the sole element was a singleton, would itself
|
||||
// be a singleton type. However, unions with length < 2 should never appear in
|
||||
// our model due to [`UnionBuilder::build`].
|
||||
false
|
||||
}
|
||||
Type::Intersection(..) => {
|
||||
// Intersection types are hard to analyze. The following types are technically
|
||||
// all singleton types, but it is not straightforward to compute this. Again,
|
||||
// we simply return false.
|
||||
//
|
||||
// bool & ~Literal[False]`
|
||||
// None & (None | int)
|
||||
// (A | B) & (B | C) with A, B, C disjunct and B a singleton
|
||||
//
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a member access of a type.
|
||||
///
|
||||
/// For example, if `foo` is `Type::Instance(<Bar>)`,
|
||||
@@ -600,9 +641,9 @@ impl<'db> Type<'db> {
|
||||
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
|
||||
match self {
|
||||
// TODO validate typed call arguments vs callable signature
|
||||
Type::Function(function_type) => match function_type.kind(db) {
|
||||
FunctionKind::Ordinary => CallOutcome::callable(function_type.return_type(db)),
|
||||
FunctionKind::RevealType => CallOutcome::revealed(
|
||||
Type::Function(function_type) => match function_type.known(db) {
|
||||
None => CallOutcome::callable(function_type.return_type(db)),
|
||||
Some(KnownFunction::RevealType) => CallOutcome::revealed(
|
||||
function_type.return_type(db),
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
),
|
||||
@@ -610,16 +651,15 @@ impl<'db> Type<'db> {
|
||||
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
Type::Class(class) => {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to return
|
||||
// the specific truthiness value of the input arg, `Literal[True]` for the example above.
|
||||
let is_bool = class.is_stdlib_symbol(db, "builtins", "bool");
|
||||
CallOutcome::callable(if is_bool {
|
||||
arg_types
|
||||
CallOutcome::callable(match class.known(db) {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
||||
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||
// the example above.
|
||||
Some(KnownClass::Bool) => arg_types
|
||||
.first()
|
||||
.map(|arg| arg.bool(db).into_type(db))
|
||||
.unwrap_or(Type::BooleanLiteral(false))
|
||||
} else {
|
||||
Type::Instance(class)
|
||||
.unwrap_or(Type::BooleanLiteral(false)),
|
||||
_ => Type::Instance(class),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -714,7 +754,7 @@ impl<'db> Type<'db> {
|
||||
let dunder_get_item_method = iterable_meta_type.member(db, "__getitem__");
|
||||
|
||||
dunder_get_item_method
|
||||
.call(db, &[self, builtins_symbol_ty(db, "int").to_instance(db)])
|
||||
.call(db, &[self, KnownClass::Int.to_instance(db)])
|
||||
.return_ty(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
@@ -758,17 +798,17 @@ impl<'db> Type<'db> {
|
||||
Type::Never => Type::Never,
|
||||
Type::Instance(class) => Type::Class(*class),
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
Type::BooleanLiteral(_) => builtins_symbol_ty(db, "bool"),
|
||||
Type::BytesLiteral(_) => builtins_symbol_ty(db, "bytes"),
|
||||
Type::IntLiteral(_) => builtins_symbol_ty(db, "int"),
|
||||
Type::Function(_) => types_symbol_ty(db, "FunctionType"),
|
||||
Type::Module(_) => types_symbol_ty(db, "ModuleType"),
|
||||
Type::Tuple(_) => builtins_symbol_ty(db, "tuple"),
|
||||
Type::None => typeshed_symbol_ty(db, "NoneType"),
|
||||
Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db),
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db),
|
||||
Type::IntLiteral(_) => KnownClass::Int.to_class(db),
|
||||
Type::Function(_) => KnownClass::FunctionType.to_class(db),
|
||||
Type::Module(_) => KnownClass::ModuleType.to_class(db),
|
||||
Type::Tuple(_) => KnownClass::Tuple.to_class(db),
|
||||
Type::None => KnownClass::NoneType.to_class(db),
|
||||
// TODO not accurate if there's a custom metaclass...
|
||||
Type::Class(_) => builtins_symbol_ty(db, "type"),
|
||||
Type::Class(_) => KnownClass::Type.to_class(db),
|
||||
// TODO can we do better here? `type[LiteralString]`?
|
||||
Type::StringLiteral(_) | Type::LiteralString => builtins_symbol_ty(db, "str"),
|
||||
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class(db),
|
||||
// TODO: `type[Any]`?
|
||||
Type::Any => Type::Todo,
|
||||
// TODO: `type[Unknown]`?
|
||||
@@ -790,7 +830,7 @@ impl<'db> Type<'db> {
|
||||
Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db),
|
||||
Type::StringLiteral(_) | Type::LiteralString => *self,
|
||||
// TODO: handle more complex types
|
||||
_ => Type::builtin_str_instance(db),
|
||||
_ => KnownClass::Str.to_instance(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,18 +842,14 @@ impl<'db> Type<'db> {
|
||||
Type::IntLiteral(number) => Type::StringLiteral(StringLiteralType::new(db, {
|
||||
number.to_string().into_boxed_str()
|
||||
})),
|
||||
Type::BooleanLiteral(true) => {
|
||||
Type::StringLiteral(StringLiteralType::new(db, "True".into()))
|
||||
}
|
||||
Type::BooleanLiteral(false) => {
|
||||
Type::StringLiteral(StringLiteralType::new(db, "False".into()))
|
||||
}
|
||||
Type::BooleanLiteral(true) => Type::StringLiteral(StringLiteralType::new(db, "True")),
|
||||
Type::BooleanLiteral(false) => Type::StringLiteral(StringLiteralType::new(db, "False")),
|
||||
Type::StringLiteral(literal) => Type::StringLiteral(StringLiteralType::new(db, {
|
||||
format!("'{}'", literal.value(db).escape_default()).into()
|
||||
format!("'{}'", literal.value(db).escape_default()).into_boxed_str()
|
||||
})),
|
||||
Type::LiteralString => Type::LiteralString,
|
||||
// TODO: handle more complex types
|
||||
_ => Type::builtin_str_instance(db),
|
||||
_ => KnownClass::Str.to_instance(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -824,6 +860,138 @@ impl<'db> From<&Type<'db>> for Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
|
||||
/// for easier syntax when interacting with very common classes.
|
||||
///
|
||||
/// Feel free to expand this enum if you ever find yourself using the same class in multiple
|
||||
/// places.
|
||||
/// Note: good candidates are any classes in `[crate::stdlib::CoreStdlibModule]`
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KnownClass {
|
||||
// To figure out where an stdlib symbol is defined, you can go into `crates/red_knot_vendored`
|
||||
// and grep for the symbol name in any `.pyi` file.
|
||||
|
||||
// Builtins
|
||||
Bool,
|
||||
Object,
|
||||
Bytes,
|
||||
Type,
|
||||
Int,
|
||||
Float,
|
||||
Str,
|
||||
List,
|
||||
Tuple,
|
||||
Set,
|
||||
Dict,
|
||||
// Types
|
||||
GenericAlias,
|
||||
ModuleType,
|
||||
FunctionType,
|
||||
// Typeshed
|
||||
NoneType, // Part of `types` for Python >= 3.10
|
||||
}
|
||||
|
||||
impl<'db> KnownClass {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Bool => "bool",
|
||||
Self::Object => "object",
|
||||
Self::Bytes => "bytes",
|
||||
Self::Tuple => "tuple",
|
||||
Self::Int => "int",
|
||||
Self::Float => "float",
|
||||
Self::Str => "str",
|
||||
Self::Set => "set",
|
||||
Self::Dict => "dict",
|
||||
Self::List => "list",
|
||||
Self::Type => "type",
|
||||
Self::GenericAlias => "GenericAlias",
|
||||
Self::ModuleType => "ModuleType",
|
||||
Self::FunctionType => "FunctionType",
|
||||
Self::NoneType => "NoneType",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.to_class(db).to_instance(db)
|
||||
}
|
||||
|
||||
pub fn to_class(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Set
|
||||
| Self::Dict => builtins_symbol_ty(db, self.as_str()),
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => {
|
||||
types_symbol_ty(db, self.as_str())
|
||||
}
|
||||
Self::NoneType => typeshed_symbol_ty(db, self.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_from_module(module: &Module, class_name: &str) -> Option<Self> {
|
||||
let candidate = Self::from_name(class_name)?;
|
||||
if candidate.check_module(module) {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn from_name(name: &str) -> Option<Self> {
|
||||
// Note: if this becomes hard to maintain (as rust can't ensure at compile time that all
|
||||
// variants of `Self` are covered), we might use a macro (in-house or dependency)
|
||||
// See: https://stackoverflow.com/q/39070244
|
||||
match name {
|
||||
"bool" => Some(Self::Bool),
|
||||
"object" => Some(Self::Object),
|
||||
"bytes" => Some(Self::Bytes),
|
||||
"tuple" => Some(Self::Tuple),
|
||||
"type" => Some(Self::Type),
|
||||
"int" => Some(Self::Int),
|
||||
"float" => Some(Self::Float),
|
||||
"str" => Some(Self::Str),
|
||||
"set" => Some(Self::Set),
|
||||
"dict" => Some(Self::Dict),
|
||||
"list" => Some(Self::List),
|
||||
"GenericAlias" => Some(Self::GenericAlias),
|
||||
"NoneType" => Some(Self::NoneType),
|
||||
"ModuleType" => Some(Self::ModuleType),
|
||||
"FunctionType" => Some(Self::FunctionType),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Private method checking if known class can be defined in the given module.
|
||||
fn check_module(self, module: &Module) -> bool {
|
||||
if !module.search_path().is_standard_library() {
|
||||
return false;
|
||||
}
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Set
|
||||
| Self::Dict => module.name() == "builtins",
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => module.name() == "types",
|
||||
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum CallOutcome<'db> {
|
||||
Callable {
|
||||
@@ -1128,7 +1296,7 @@ impl Truthiness {
|
||||
match self {
|
||||
Self::AlwaysTrue => Type::BooleanLiteral(true),
|
||||
Self::AlwaysFalse => Type::BooleanLiteral(false),
|
||||
Self::Ambiguous => builtins_symbol_ty(db, "bool").to_instance(db),
|
||||
Self::Ambiguous => KnownClass::Bool.to_instance(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1150,7 +1318,7 @@ pub struct FunctionType<'db> {
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// Is this a function that we special-case somehow? If so, which one?
|
||||
kind: FunctionKind,
|
||||
known: Option<KnownFunction>,
|
||||
|
||||
definition: Definition<'db>,
|
||||
|
||||
@@ -1202,11 +1370,10 @@ impl<'db> FunctionType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Hash)]
|
||||
pub enum FunctionKind {
|
||||
/// Just a normal function for which we have no particular special casing
|
||||
#[default]
|
||||
Ordinary,
|
||||
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might
|
||||
/// have special behavior.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum KnownFunction {
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
}
|
||||
@@ -1220,9 +1387,18 @@ pub struct ClassType<'db> {
|
||||
definition: Definition<'db>,
|
||||
|
||||
body_scope: ScopeId<'db>,
|
||||
|
||||
known: Option<KnownClass>,
|
||||
}
|
||||
|
||||
impl<'db> ClassType<'db> {
|
||||
pub fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
match self.known(db) {
|
||||
Some(known) => known == known_class,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this class is a standard library type with given module name and name.
|
||||
pub(crate) fn is_stdlib_symbol(self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
name == self.name(db)
|
||||
@@ -1334,6 +1510,12 @@ pub struct StringLiteralType<'db> {
|
||||
value: Box<str>,
|
||||
}
|
||||
|
||||
impl<'db> StringLiteralType<'db> {
|
||||
pub fn len(&self, db: &'db dyn Db) -> usize {
|
||||
self.value(db).len()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct BytesLiteralType<'db> {
|
||||
#[return_ref]
|
||||
@@ -1346,6 +1528,16 @@ pub struct TupleType<'db> {
|
||||
elements: Box<[Type<'db>]>,
|
||||
}
|
||||
|
||||
impl<'db> TupleType<'db> {
|
||||
pub fn get(&self, db: &'db dyn Db, index: usize) -> Option<Type<'db>> {
|
||||
self.elements(db).get(index).copied()
|
||||
}
|
||||
|
||||
pub fn len(&self, db: &'db dyn Db) -> usize {
|
||||
self.elements(db).len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
@@ -1385,6 +1577,7 @@ mod tests {
|
||||
enum Ty {
|
||||
Never,
|
||||
Unknown,
|
||||
None,
|
||||
Any,
|
||||
IntLiteral(i64),
|
||||
BoolLiteral(bool),
|
||||
@@ -1401,22 +1594,19 @@ mod tests {
|
||||
match self {
|
||||
Ty::Never => Type::Never,
|
||||
Ty::Unknown => Type::Unknown,
|
||||
Ty::None => Type::None,
|
||||
Ty::Any => Type::Any,
|
||||
Ty::IntLiteral(n) => Type::IntLiteral(n),
|
||||
Ty::StringLiteral(s) => {
|
||||
Type::StringLiteral(StringLiteralType::new(db, (*s).into()))
|
||||
}
|
||||
Ty::StringLiteral(s) => Type::StringLiteral(StringLiteralType::new(db, s)),
|
||||
Ty::BoolLiteral(b) => Type::BooleanLiteral(b),
|
||||
Ty::LiteralString => Type::LiteralString,
|
||||
Ty::BytesLiteral(s) => {
|
||||
Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes().into()))
|
||||
}
|
||||
Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
}
|
||||
Ty::Tuple(tys) => {
|
||||
let elements = tys.into_iter().map(|ty| ty.into_type(db)).collect();
|
||||
let elements: Box<_> = tys.into_iter().map(|ty| ty.into_type(db)).collect();
|
||||
Type::Tuple(TupleType::new(db, elements))
|
||||
}
|
||||
}
|
||||
@@ -1489,6 +1679,28 @@ mod tests {
|
||||
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
|
||||
}
|
||||
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BoolLiteral(true))]
|
||||
#[test_case(Ty::BoolLiteral(false))]
|
||||
#[test_case(Ty::Tuple(vec![]))]
|
||||
fn is_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never)]
|
||||
#[test_case(Ty::IntLiteral(345))]
|
||||
#[test_case(Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))]
|
||||
fn is_not_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(!from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
|
||||
#[test_case(Ty::IntLiteral(-1))]
|
||||
#[test_case(Ty::StringLiteral("foo"))]
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::KnownClass;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -64,7 +66,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
for (index, element) in self.elements.iter().enumerate() {
|
||||
if Some(*element) == bool_pair {
|
||||
to_add = builtins_symbol_ty(self.db, "bool");
|
||||
to_add = KnownClass::Bool.to_instance(self.db);
|
||||
to_remove.push(index);
|
||||
// The type we are adding is a BooleanLiteral, which doesn't have any
|
||||
// subtypes. And we just found that the union already contained our
|
||||
@@ -109,7 +111,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
match self.elements.len() {
|
||||
0 => Type::Never,
|
||||
1 => self.elements[0],
|
||||
_ => Type::Union(UnionType::new(self.db, self.elements.into())),
|
||||
_ => Type::Union(UnionType::new(self.db, self.elements.into_boxed_slice())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +302,7 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::{builtins_symbol_ty, UnionBuilder};
|
||||
use crate::types::{KnownClass, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
@@ -360,7 +362,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_ty = builtins_symbol_ty(&db, "bool");
|
||||
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
@@ -371,7 +373,7 @@ mod tests {
|
||||
assert_eq!(union.elements(&db), &[t0, t3]);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[bool_ty, t3]);
|
||||
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -389,7 +391,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_simplify_subtype() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::builtin_str_instance(&db);
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::LiteralString;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
@@ -401,7 +403,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_no_simplify_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::builtin_str_instance(&db);
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
@@ -413,9 +415,9 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
let str_ty = Type::builtin_str_instance(&db);
|
||||
let int_ty = Type::builtin_int_instance(&db);
|
||||
let object_ty = builtins_symbol_ty(&db, "object").to_instance(&db);
|
||||
let str_ty = KnownClass::Str.to_instance(&db);
|
||||
let int_ty = KnownClass::Int.to_instance(&db);
|
||||
let object_ty = KnownClass::Object.to_instance(&db);
|
||||
let unknown_ty = Type::Unknown;
|
||||
|
||||
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
|
||||
|
||||
@@ -341,12 +341,12 @@ mod tests {
|
||||
Type::Unknown,
|
||||
Type::IntLiteral(-1),
|
||||
global_symbol_ty(&db, mod_file, "A"),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, Box::from("A"))),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([0]))),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([7]))),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, "A")),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, [0u8].as_slice())),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, [7u8].as_slice())),
|
||||
Type::IntLiteral(0),
|
||||
Type::IntLiteral(1),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, Box::from("B"))),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, "B")),
|
||||
global_symbol_ty(&db, mod_file, "foo"),
|
||||
global_symbol_ty(&db, mod_file, "bar"),
|
||||
global_symbol_ty(&db, mod_file, "B"),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -155,13 +155,24 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
|
||||
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
|
||||
if matches!(op, ast::CmpOp::IsNot) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
};
|
||||
// TODO other comparison types
|
||||
match op {
|
||||
ast::CmpOp::IsNot => {
|
||||
if comp_ty.is_singleton(self.db) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
} else {
|
||||
// Non-singletons cannot be safely narrowed using `is not`
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Is => {
|
||||
self.constraints.insert(symbol, comp_ty);
|
||||
}
|
||||
_ => {
|
||||
// TODO other comparison types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
crates/red_knot_python_semantic/tests/mdtest.rs
Normal file
14
crates/red_knot_python_semantic/tests/mdtest.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use red_knot_test::run;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// See `crates/red_knot_test/README.md` for documentation on these tests.
|
||||
#[rstest::rstest]
|
||||
fn mdtest(#[files("resources/mdtest/**/*.md")] path: PathBuf) {
|
||||
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("mdtest")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let title = path.strip_prefix(crate_dir).unwrap();
|
||||
run(&path, title.as_os_str().to_str().unwrap());
|
||||
}
|
||||
33
crates/red_knot_test/Cargo.toml
Normal file
33
crates/red_knot_test/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "red_knot_test"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
470
crates/red_knot_test/README.md
Normal file
470
crates/red_knot_test/README.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# Writing type-checking / type-inference tests
|
||||
|
||||
Any Markdown file can be a test suite.
|
||||
|
||||
In order for it to be run as one, `red_knot_test::run` must be called with its path; see
|
||||
`crates/red_knot_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files
|
||||
under a certain directory as test suites.
|
||||
|
||||
A Markdown test suite can contain any number of tests. A test consists of one or more embedded
|
||||
"files", each defined by a triple-backticks fenced code block. The code block must have a tag string
|
||||
specifying its language; currently only `py` (Python files) and `pyi` (type stub files) are
|
||||
supported.
|
||||
|
||||
The simplest possible test suite consists of just a single test, with a single embedded file:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
````
|
||||
|
||||
When running this test, the mdtest framework will write a file with these contents to the default
|
||||
file path (`/src/test.py`) in its in-memory file system, run a type check on that file, and then
|
||||
match the resulting diagnostics with the assertions in the test. Assertions are in the form of
|
||||
Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, it
|
||||
fails.
|
||||
|
||||
<!---
|
||||
(If you are reading this document in raw Markdown source rather than rendered Markdown, note that
|
||||
the quadruple-backtick-fenced "markdown" language code block above is NOT itself part of the mdtest
|
||||
syntax, it's just how this README embeds an example mdtest Markdown document.)
|
||||
--->
|
||||
|
||||
See actual example mdtest suites in
|
||||
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
|
||||
|
||||
> ℹ️ Note: If you use `rstest` to generate a separate test for all Markdown files in a certain directory,
|
||||
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
|
||||
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
|
||||
> Markdown file is added or removed from the directory. See
|
||||
> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs)
|
||||
> for an example of how to do this.
|
||||
>
|
||||
> This is because `rstest` generates its tests at build time rather than at runtime.
|
||||
> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed,
|
||||
> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild
|
||||
> following the addition of the new test file.
|
||||
|
||||
## Assertions
|
||||
|
||||
Two kinds of assertions are supported: `# revealed:` (shown above) and `# error:`.
|
||||
|
||||
### Assertion kinds
|
||||
|
||||
#### revealed
|
||||
|
||||
A `# revealed:` assertion should always be paired with a call to the `reveal_type` utility, which
|
||||
reveals (via a diagnostic) the inferred type of its argument (which can be any expression). The text
|
||||
after `# revealed:` must match exactly with the displayed form of the revealed type of that
|
||||
expression.
|
||||
|
||||
The `reveal_type` function can be imported from the `typing` standard library module (or, for older
|
||||
Python versions, from the `typing_extensions` pseudo-standard-library module[^extensions]):
|
||||
|
||||
```py
|
||||
from typing import reveal_type
|
||||
|
||||
reveal_type("foo") # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
For convenience, type checkers also pretend that `reveal_type` is a built-in, so that this import is
|
||||
not required. Using `reveal_type` without importing it issues a diagnostic warning that it was used
|
||||
without importing it, in addition to the diagnostic revealing the type of the expression.
|
||||
|
||||
The `# revealed:` assertion must always match a revealed-type diagnostic, and will also match the
|
||||
undefined-reveal diagnostic, if present, so it's safe to use `reveal_type` in tests either with or
|
||||
without importing it. (Style preference is to not import it in tests, unless specifically testing
|
||||
something about the behavior of importing it.)
|
||||
|
||||
#### error
|
||||
|
||||
A comment beginning with `# error:` is an assertion that a type checker diagnostic will be emitted,
|
||||
with text span starting on that line. The matching can be narrowed in three ways:
|
||||
|
||||
- `# error: [invalid-assignment]` requires that the matched diagnostic have the rule code
|
||||
`invalid-assignment`. (The square brackets are required.)
|
||||
- `# error: "Some text"` requires that the diagnostic's full message contain the text `Some text`.
|
||||
(The double quotes are required in the assertion comment; they are not part of the matched text.)
|
||||
- `# error: 8 [rule-code]` or `# error: 8 "Some text"` additionally requires that the matched
|
||||
diagnostic's text span begins on column 8 (one-indexed) of this line.
|
||||
|
||||
Assertions must contain either a rule code or a contains-text, or both, and may optionally also
|
||||
include a column number. They must come in order: first column, if present; then rule code, if
|
||||
present; then contains-text, if present. For example, an assertion using all three would look like
|
||||
`# error: 8 [invalid-assignment] "Some text"`.
|
||||
|
||||
Error assertions in tests intended to test type checker semantics should primarily use rule-code
|
||||
assertions, with occasional contains-text assertions where needed to disambiguate or validate some
|
||||
details of the diagnostic message.
|
||||
|
||||
### Assertion locations
|
||||
|
||||
An assertion comment may be a line-trailing comment, in which case it applies to the line it is on:
|
||||
|
||||
```py
|
||||
x: str = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
Or it may be a comment on its own line, in which case it applies to the next line that does not
|
||||
contain an assertion comment:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment]
|
||||
x: str = 1
|
||||
```
|
||||
|
||||
Multiple assertions applying to the same line may be stacked:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment]
|
||||
# revealed: Literal[1]
|
||||
x: str = reveal_type(1)
|
||||
```
|
||||
|
||||
Intervening empty lines or non-assertion comments are not allowed; an assertion stack must be one
|
||||
assertion per line, immediately following each other, with the line immediately following the last
|
||||
assertion as the line of source code on which the matched diagnostics are emitted.
|
||||
|
||||
## Multi-file tests
|
||||
|
||||
Some tests require multiple files, with imports from one file into another. Multiple fenced code
|
||||
blocks represent multiple embedded files. Since files must have unique names, at most one file can
|
||||
use the default name of `/src/test.py`. Other files must explicitly specify their file name:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
from b import C
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
```
|
||||
````
|
||||
|
||||
Relative file names are always relative to the "workspace root", which is also an import root (that
|
||||
is, the equivalent of a runtime entry on `sys.path`).
|
||||
|
||||
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
|
||||
this is a feature we will want to add in the future.
|
||||
|
||||
So the above test creates two files, `/src/test.py` and `/src/b.py`, and sets the workspace root to
|
||||
`/src/`, allowing `test.py` to import from `b.py` using the module name `b`.
|
||||
|
||||
## Multi-test suites
|
||||
|
||||
A single test suite (Markdown file) can contain multiple tests, by demarcating them using Markdown
|
||||
header lines:
|
||||
|
||||
````markdown
|
||||
# Same-file invalid assignment
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Cross-file invalid assignment
|
||||
|
||||
```py
|
||||
from b import y
|
||||
x: int = y # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
y = "foo"
|
||||
```
|
||||
````
|
||||
|
||||
This test suite contains two tests, one named "Same-file invalid assignment" and the other named
|
||||
"Cross-file invalid assignment". The first test involves only a single embedded file, and the second
|
||||
test involves two embedded files.
|
||||
|
||||
The tests are run independently, in independent in-memory file systems and with new red-knot
|
||||
[Salsa](https://github.com/salsa-rs/salsa) databases. This means that each is a from-scratch run of
|
||||
the type checker, with no data persisting from any previous test.
|
||||
|
||||
Due to `cargo test` limitations, an entire test suite (Markdown file) is run as a single Rust test,
|
||||
so it's not possible to select individual tests within it to run.
|
||||
|
||||
## Structured test suites
|
||||
|
||||
Markdown headers can also be used to group related tests within a suite:
|
||||
|
||||
````markdown
|
||||
# Literals
|
||||
|
||||
## Numbers
|
||||
|
||||
### Integer
|
||||
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Float
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
```py
|
||||
reveal_type("foo") # revealed: Literal["foo"]
|
||||
```
|
||||
````
|
||||
|
||||
This test suite contains three tests, named "Literals - Numbers - Integer", "Literals - Numbers -
|
||||
Float", and "Literals - Strings".
|
||||
|
||||
A header-demarcated section must either be a test or a grouping header; it cannot be both. That is,
|
||||
a header section can either contain embedded files (making it a test), or it can contain more
|
||||
deeply-nested headers (headers with more `#`), but it cannot contain both.
|
||||
|
||||
## Documentation of tests
|
||||
|
||||
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
|
||||
the test framework) between fenced code blocks. This permits natural documentation of
|
||||
why a test exists, and what it intends to assert:
|
||||
|
||||
````markdown
|
||||
Assigning a string to a variable annotated as `int` is not permitted:
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
````
|
||||
|
||||
## Planned features
|
||||
|
||||
There are some designed features that we intend for the test framework to have, but have not yet
|
||||
implemented:
|
||||
|
||||
### Multi-line diagnostic assertions
|
||||
|
||||
We may want to be able to assert that a diagnostic spans multiple lines, and to assert the columns it
|
||||
begins and/or ends on. The planned syntax for this will use `<<<` and `>>>` to mark the start and end lines for
|
||||
an assertion:
|
||||
|
||||
```py
|
||||
(3 # error: 2 [unsupported-operands] <<<
|
||||
+
|
||||
"foo") # error: 6 >>>
|
||||
```
|
||||
|
||||
The column assertion `6` on the ending line should be optional.
|
||||
|
||||
In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins
|
||||
an assertion ended by `>>>>`, etc.
|
||||
|
||||
### Non-Python files
|
||||
|
||||
Some tests may need to specify non-Python embedded files: typeshed `stdlib/VERSIONS`, `pth` files,
|
||||
`py.typed` files, `pyvenv.cfg` files...
|
||||
|
||||
We will allow specifying any of these using the `text` language in the code block tag string:
|
||||
|
||||
````markdown
|
||||
```text path=/third-party/foo/py.typed
|
||||
partial
|
||||
```
|
||||
````
|
||||
|
||||
We may want to also support testing Jupyter notebooks as embedded files; exact syntax for this is
|
||||
yet to be determined.
|
||||
|
||||
Of course, red-knot is only run directly on `py` and `pyi` files, and assertion comments are only
|
||||
possible in these files.
|
||||
|
||||
A fenced code block with no language will always be an error.
|
||||
|
||||
### Configuration
|
||||
|
||||
We will add the ability to specify non-default red-knot configurations to use in tests, by including
|
||||
a TOML code block:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot]
|
||||
warn-on-any = true
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any): # error: [use-of-any]
|
||||
pass
|
||||
```
|
||||
````
|
||||
|
||||
It should be possible to include a TOML code block in a single test (as shown), or in a grouping
|
||||
section, in which case it applies to all nested tests within that grouping section. Configurations
|
||||
at multiple level are allowed and merged, with the most-nested (closest to the test) taking
|
||||
precedence.
|
||||
|
||||
### Running just a single test from a suite
|
||||
|
||||
Having each test in a suite always run as a distinct Rust test would require writing our own test
|
||||
runner or code-generating tests in a build script; neither of these is planned.
|
||||
|
||||
We could still allow running just a single test from a suite, for debugging purposes, either via
|
||||
some "focus" syntax that could be easily temporarily added to a test, or via an environment
|
||||
variable.
|
||||
|
||||
### Configuring search paths and kinds
|
||||
|
||||
The red-knot TOML configuration format hasn't been designed yet, and we may want to implement
|
||||
support in the test framework for configuring search paths before it is designed. If so, we can
|
||||
define some configuration options for now under the `[tool.knot.tests]` namespace. In the future,
|
||||
perhaps some of these can be replaced by real red-knot configuration options; some or all may also
|
||||
be kept long-term as test-specific options.
|
||||
|
||||
Some configuration options we will want to provide:
|
||||
|
||||
- We should be able to configure the default workspace root to something other than `/src/` using a
|
||||
`workspace-root` configuration option.
|
||||
|
||||
- We should be able to add a third-party root using the `third-party-root` configuration option.
|
||||
|
||||
- We may want to add additional configuration options for setting additional search path kinds.
|
||||
|
||||
Paths for `workspace-root` and `third-party-root` must be absolute.
|
||||
|
||||
Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a
|
||||
non-default value using the `workspace-root` config.
|
||||
|
||||
### Specifying a custom typeshed
|
||||
|
||||
Some tests will need to override the default typeshed with custom files. The `[tool.knot.tests]`
|
||||
configuration option `typeshed-root` should be usable for this:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot.tests]
|
||||
typeshed-root = "/typeshed"
|
||||
```
|
||||
|
||||
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we
|
||||
configured above as our custom typeshed root:
|
||||
|
||||
```py path=/typeshed/stdlib/builtins.pyi
|
||||
I_AM_THE_ONLY_BUILTIN = 1
|
||||
```
|
||||
|
||||
This file is written to `/src/test.py`, because the default workspace root is `/src/ and the default
|
||||
file path is `test.py`:
|
||||
|
||||
```py
|
||||
reveal_type(I_AM_THE_ONLY_BUILTIN) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
|
||||
custom typeshed root. If no such file is created explicitly, one should be created implicitly
|
||||
including entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python
|
||||
versions.
|
||||
|
||||
### I/O errors
|
||||
|
||||
We could use an `error=` configuration option in the tag string to make an embedded file cause an
|
||||
I/O error on read.
|
||||
|
||||
### Asserting on full diagnostic output
|
||||
|
||||
The inline comment diagnostic assertions are useful for making quick, readable assertions about
|
||||
diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic
|
||||
output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will
|
||||
want to assert on diagnostics in a file, without impacting the contents of that file by changing a
|
||||
comment in it. In these cases, a Python code block in a test could be followed by a fenced code
|
||||
block with language `output`; this would contain the full diagnostic output for the preceding test
|
||||
file:
|
||||
|
||||
````markdown
|
||||
# full output
|
||||
|
||||
```py
|
||||
x = 1
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
This is just an example, not a proposal that red-knot would ever actually output diagnostics in
|
||||
precisely this format:
|
||||
|
||||
```output
|
||||
test.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
````
|
||||
|
||||
We will want to build tooling to automatically capture and update these “full diagnostic output”
|
||||
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
|
||||
|
||||
By default, an `output` block will specify diagnostic output for the file `<workspace-root>/test.py`.
|
||||
An `output` block can have a `path=` option, to explicitly specify the Python file for which it
|
||||
asserts diagnostic output, and a `stage=` option, to specify which stage of an incremental test it
|
||||
specifies diagnostic output at. (See “incremental tests” below.)
|
||||
|
||||
It is an error for an `output` block to exist, if there is no `py` or `python` block in the same
|
||||
test for the same file path.
|
||||
|
||||
### Incremental tests
|
||||
|
||||
Some tests should validate incremental checking, by initially creating some files, checking them,
|
||||
and then modifying/adding/deleting files and checking again.
|
||||
|
||||
We should add the capability to create an incremental test by using the `stage=` option on some
|
||||
fenced code blocks in the test:
|
||||
|
||||
````markdown
|
||||
# Incremental
|
||||
|
||||
## modify a file
|
||||
|
||||
Initial version of `test.py` and `b.py`:
|
||||
|
||||
```py
|
||||
from b import x
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
Initial expected output for `test.py`:
|
||||
|
||||
```output
|
||||
/src/test.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
|
||||
Now in our first incremental stage, modify the contents of `b.py`:
|
||||
|
||||
```py path=b.py stage=1
|
||||
# b.py
|
||||
x = 2
|
||||
```
|
||||
|
||||
And this is our updated expected output for `test.py` at stage 1:
|
||||
|
||||
```output stage=1
|
||||
/src/test.py, line 1, col 1: revealed type is 'Literal[2]'
|
||||
```
|
||||
|
||||
(One reason to use full-diagnostic-output blocks in this test is that updating
|
||||
inline-comment diagnostic assertions for `test.py` would require specifying new
|
||||
contents for `test.py` in stage 1, which we don't want to do in this test.)
|
||||
````
|
||||
|
||||
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies
|
||||
a filename that was specified in a previous stage (or the initial stage), that file is modified. A
|
||||
new filename appearing for the first time in a new stage will create a new file. To delete a
|
||||
previously created file, specify that file with the tag `delete` in its tag string (in this case, it
|
||||
is an error to provide non-empty contents). Any previously-created files that are not re-specified
|
||||
in a later stage continue to exist with their previously-specified contents, and are not "touched".
|
||||
|
||||
All stages should be run in order, incrementally, and then the final state should also be re-checked
|
||||
cold, to validate equivalence of cold and incremental check results.
|
||||
|
||||
[^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers
|
||||
also, treat it as part of the standard library.
|
||||
621
crates/red_knot_test/src/assertion.rs
Normal file
621
crates/red_knot_test/src/assertion.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
//! Parse type and type-error assertions in Python comment form.
|
||||
//!
|
||||
//! Parses comments of the form `# revealed: SomeType` and `# error: 8 [rule-code] "message text"`.
|
||||
//! In the latter case, the `8` is a column number, and `"message text"` asserts that the full
|
||||
//! diagnostic message contains the text `"message text"`; all three are optional (`# error:` will
|
||||
//! match any error.)
|
||||
//!
|
||||
//! Assertion comments may be placed at end-of-line:
|
||||
//!
|
||||
//! ```py
|
||||
//! x: int = "foo" # error: [invalid-assignment]
|
||||
//! ```
|
||||
//!
|
||||
//! Or as a full-line comment on the preceding line:
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [invalid-assignment]
|
||||
//! x: int = "foo"
|
||||
//! ```
|
||||
//!
|
||||
//! Multiple assertion comments may apply to the same line; in this case all (or all but the last)
|
||||
//! must be full-line comments:
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [unbound-name]
|
||||
//! reveal_type(x) # revealed: Unbound
|
||||
//! ```
|
||||
//!
|
||||
//! or
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [unbound-name]
|
||||
//! # revealed: Unbound
|
||||
//! reveal_type(x)
|
||||
//! ```
|
||||
|
||||
use crate::db::Db;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::{LineIndex, Locator, OneIndexed};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Diagnostic assertion comments in a single embedded file.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct InlineFileAssertions {
|
||||
comment_ranges: CommentRanges,
|
||||
source: SourceText,
|
||||
lines: LineIndex,
|
||||
}
|
||||
|
||||
impl InlineFileAssertions {
|
||||
pub(crate) fn from_file(db: &Db, file: File) -> Self {
|
||||
let source = source_text(db, file);
|
||||
let lines = line_index(db, file);
|
||||
let parsed = parsed_module(db, file);
|
||||
let comment_ranges = CommentRanges::from(parsed.tokens());
|
||||
Self {
|
||||
comment_ranges,
|
||||
source,
|
||||
lines,
|
||||
}
|
||||
}
|
||||
|
||||
fn locator(&self) -> Locator {
|
||||
Locator::with_index(&self.source, self.lines.clone())
|
||||
}
|
||||
|
||||
fn line_number(&self, range: &impl Ranged) -> OneIndexed {
|
||||
self.lines.line_index(range.start())
|
||||
}
|
||||
|
||||
fn is_own_line_comment(&self, ranged_assertion: &AssertionWithRange) -> bool {
|
||||
CommentRanges::is_own_line(ranged_assertion.start(), &self.locator())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a InlineFileAssertions {
|
||||
type Item = LineAssertions<'a>;
|
||||
type IntoIter = LineAssertionsIterator<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Self::IntoIter {
|
||||
file_assertions: self,
|
||||
inner: AssertionWithRangeIterator {
|
||||
file_assertions: self,
|
||||
inner: self.comment_ranges.into_iter(),
|
||||
}
|
||||
.peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Assertion`] with the [`TextRange`] of its original inline comment.
|
||||
#[derive(Debug)]
|
||||
struct AssertionWithRange<'a>(Assertion<'a>, TextRange);
|
||||
|
||||
impl<'a> Deref for AssertionWithRange<'a> {
|
||||
type Target = Assertion<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AssertionWithRange<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AssertionWithRange<'a>> for Assertion<'a> {
|
||||
fn from(value: AssertionWithRange<'a>) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that yields all assertions within a single embedded Python file.
|
||||
#[derive(Debug)]
|
||||
struct AssertionWithRangeIterator<'a> {
|
||||
file_assertions: &'a InlineFileAssertions,
|
||||
inner: std::iter::Copied<std::slice::Iter<'a, TextRange>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AssertionWithRangeIterator<'a> {
|
||||
type Item = AssertionWithRange<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let locator = self.file_assertions.locator();
|
||||
loop {
|
||||
let inner_next = self.inner.next()?;
|
||||
let comment = locator.slice(inner_next);
|
||||
if let Some(assertion) = Assertion::from_comment(comment) {
|
||||
return Some(AssertionWithRange(assertion, inner_next));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for AssertionWithRangeIterator<'_> {}
|
||||
|
||||
/// A vector of [`Assertion`]s belonging to a single line.
|
||||
///
|
||||
/// Most lines will have zero or one assertion, so we use a [`SmallVec`] optimized for a single
|
||||
/// element to avoid most heap vector allocations.
|
||||
type AssertionVec<'a> = SmallVec<[Assertion<'a>; 1]>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineAssertionsIterator<'a> {
|
||||
file_assertions: &'a InlineFileAssertions,
|
||||
inner: std::iter::Peekable<AssertionWithRangeIterator<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LineAssertionsIterator<'a> {
|
||||
type Item = LineAssertions<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let file = self.file_assertions;
|
||||
let ranged_assertion = self.inner.next()?;
|
||||
let mut collector = AssertionVec::new();
|
||||
let mut line_number = file.line_number(&ranged_assertion);
|
||||
// Collect all own-line comments on consecutive lines; these all apply to the same line of
|
||||
// code. For example:
|
||||
//
|
||||
// ```py
|
||||
// # error: [unbound-name]
|
||||
// # revealed: Unbound
|
||||
// reveal_type(x)
|
||||
// ```
|
||||
//
|
||||
if file.is_own_line_comment(&ranged_assertion) {
|
||||
collector.push(ranged_assertion.into());
|
||||
let mut only_own_line = true;
|
||||
while let Some(ranged_assertion) = self.inner.peek() {
|
||||
let next_line_number = line_number.saturating_add(1);
|
||||
if file.line_number(ranged_assertion) == next_line_number {
|
||||
if !file.is_own_line_comment(ranged_assertion) {
|
||||
only_own_line = false;
|
||||
}
|
||||
line_number = next_line_number;
|
||||
collector.push(self.inner.next().unwrap().into());
|
||||
// If we see an end-of-line comment, it has to be the end of the stack,
|
||||
// otherwise we'd botch this case, attributing all three errors to the `bar`
|
||||
// line:
|
||||
//
|
||||
// ```py
|
||||
// # error:
|
||||
// foo # error:
|
||||
// bar # error:
|
||||
// ```
|
||||
//
|
||||
if !only_own_line {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if only_own_line {
|
||||
// The collected comments apply to the _next_ line in the code.
|
||||
line_number = line_number.saturating_add(1);
|
||||
}
|
||||
} else {
|
||||
// We have a line-trailing comment; it applies to its own line, and is not grouped.
|
||||
collector.push(ranged_assertion.into());
|
||||
}
|
||||
Some(LineAssertions {
|
||||
line_number,
|
||||
assertions: collector,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for LineAssertionsIterator<'_> {}
|
||||
|
||||
/// One or more assertions referring to the same line of code.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineAssertions<'a> {
|
||||
/// The line these assertions refer to.
|
||||
///
|
||||
/// Not necessarily the same line the assertion comment is located on; for an own-line comment,
|
||||
/// it's the next non-assertion line.
|
||||
pub(crate) line_number: OneIndexed,
|
||||
|
||||
/// The assertions referring to this line.
|
||||
pub(crate) assertions: AssertionVec<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for LineAssertions<'a> {
|
||||
type Target = [Assertion<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.assertions
|
||||
}
|
||||
}
|
||||
|
||||
static TYPE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
|
||||
|
||||
static ERROR_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^#\s*error:(\s*(?<column>\d+))?(\s*\[(?<rule>.+?)\])?(\s*"(?<message>.+?)")?\s*$"#,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// A single diagnostic assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Assertion<'a> {
|
||||
/// A `revealed: ` assertion.
|
||||
Revealed(&'a str),
|
||||
|
||||
/// An `error: ` assertion.
|
||||
Error(ErrorAssertion<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Assertion<'a> {
|
||||
fn from_comment(comment: &'a str) -> Option<Self> {
|
||||
if let Some(caps) = TYPE_RE.captures(comment) {
|
||||
Some(Self::Revealed(caps.name("ty_display").unwrap().as_str()))
|
||||
} else {
|
||||
ERROR_RE.captures(comment).map(|caps| {
|
||||
Self::Error(ErrorAssertion {
|
||||
rule: caps.name("rule").map(|m| m.as_str()),
|
||||
column: caps.name("column").and_then(|m| m.as_str().parse().ok()),
|
||||
message_contains: caps.name("message").map(|m| m.as_str()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Assertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Revealed(expected_type) => write!(f, "revealed: {expected_type}"),
|
||||
Self::Error(assertion) => assertion.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An `error: ` assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ErrorAssertion<'a> {
|
||||
/// The diagnostic rule code we expect.
|
||||
pub(crate) rule: Option<&'a str>,
|
||||
|
||||
/// The column we expect the diagnostic range to start at.
|
||||
pub(crate) column: Option<OneIndexed>,
|
||||
|
||||
/// A string we expect to be contained in the diagnostic message.
|
||||
pub(crate) message_contains: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ErrorAssertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("error:")?;
|
||||
if let Some(column) = self.column {
|
||||
write!(f, " {column}")?;
|
||||
}
|
||||
if let Some(rule) = self.rule {
|
||||
write!(f, " [{rule}]")?;
|
||||
}
|
||||
if let Some(message) = self.message_contains {
|
||||
write!(f, r#" "{message}""#)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Assertion, InlineFileAssertions, LineAssertions};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
fn get_assertions(source: &str) -> InlineFileAssertions {
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
InlineFileAssertions::from_file(&db, file)
|
||||
}
|
||||
|
||||
fn as_vec(assertions: &InlineFileAssertions) -> Vec<LineAssertions> {
|
||||
assertions.into_iter().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ty_display() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "revealed: Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error:
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prior_line() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Literal[1]
|
||||
reveal_type(1)
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "revealed: Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_prior_line() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Unbound
|
||||
# error: [unbound-name]
|
||||
reveal_type(x)
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [assert1, assert2] = &line.assertions[..] else {
|
||||
panic!("expected two assertions");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert1}"), "revealed: Unbound");
|
||||
assert_eq!(format!("{assert2}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_mixed() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Unbound
|
||||
reveal_type(x) # error: [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert1, assert2] = &line.assertions[..] else {
|
||||
panic!("expected two assertions");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert1}"), "revealed: Unbound");
|
||||
assert_eq!(format!("{assert2}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lines() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [invalid-assignment]
|
||||
x: int = "foo"
|
||||
y # error: [unbound-name]
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line1, line2] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2));
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [Assertion::Error(error1)] = &line1.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error1.rule, Some("invalid-assignment"));
|
||||
|
||||
let [Assertion::Error(error2)] = &line2.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error2.rule, Some("unbound-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lines_mixed_stack() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [invalid-assignment]
|
||||
x: int = reveal_type("foo") # revealed: str
|
||||
y # error: [unbound-name]
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line1, line2] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2));
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [Assertion::Error(error1), Assertion::Revealed(expected_ty)] = &line1.assertions[..]
|
||||
else {
|
||||
panic!("expected one error assertion and one Revealed assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error1.rule, Some("invalid-assignment"));
|
||||
assert_eq!(*expected_ty, "str");
|
||||
|
||||
let [Assertion::Error(error2)] = &line2.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error2.rule, Some("unbound-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error: [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error: 1 [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error: 1 [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_message() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [unbound-name] "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format!("{assert}"),
|
||||
r#"error: [unbound-name] "`x` is unbound""#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_message_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: 1 "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), r#"error: 1 "`x` is unbound""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_message_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: 1 [unbound-name] "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format!("{assert}"),
|
||||
r#"error: 1 [unbound-name] "`x` is unbound""#
|
||||
);
|
||||
}
|
||||
}
|
||||
88
crates/red_knot_test/src/db.rs
Normal file
88
crates/red_knot_test/src/db.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use red_knot_python_semantic::{
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonVersion, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct Db {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let db = Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&workspace_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(workspace_root),
|
||||
},
|
||||
)
|
||||
.expect("Invalid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for Db {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for Db {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for Db {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for Db {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for Db {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
||||
173
crates/red_knot_test/src/diagnostic.rs
Normal file
173
crates/red_knot_test/src/diagnostic.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Sort and group diagnostics by line number, so they can be correlated with assertions.
|
||||
//!
|
||||
//! We don't assume that we will get the diagnostics in source order.
|
||||
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::ops::{Deref, Range};
|
||||
|
||||
/// All diagnostics for one embedded Python file, sorted and grouped by start line number.
|
||||
///
|
||||
/// The diagnostics are kept in a flat vector, sorted by line number. A separate vector of
|
||||
/// [`LineDiagnosticRange`] has one entry for each contiguous slice of the diagnostics vector
|
||||
/// containing diagnostics which all start on the same line.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SortedDiagnostics<T> {
|
||||
diagnostics: Vec<T>,
|
||||
line_ranges: Vec<LineDiagnosticRange>,
|
||||
}
|
||||
|
||||
impl<T> SortedDiagnostics<T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
{
|
||||
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
||||
let mut diagnostics: Vec<_> = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| DiagnosticWithLine {
|
||||
line_number: line_index.line_index(diagnostic.start()),
|
||||
diagnostic,
|
||||
})
|
||||
.collect();
|
||||
diagnostics.sort_unstable_by_key(|diagnostic_with_line| diagnostic_with_line.line_number);
|
||||
|
||||
let mut diags = Self {
|
||||
diagnostics: Vec::with_capacity(diagnostics.len()),
|
||||
line_ranges: vec![],
|
||||
};
|
||||
|
||||
let mut current_line_number = None;
|
||||
let mut start = 0;
|
||||
for DiagnosticWithLine {
|
||||
line_number,
|
||||
diagnostic,
|
||||
} in diagnostics
|
||||
{
|
||||
match current_line_number {
|
||||
None => {
|
||||
current_line_number = Some(line_number);
|
||||
}
|
||||
Some(current) => {
|
||||
if line_number != current {
|
||||
let end = diags.diagnostics.len();
|
||||
diags.line_ranges.push(LineDiagnosticRange {
|
||||
line_number: current,
|
||||
diagnostic_index_range: start..end,
|
||||
});
|
||||
start = end;
|
||||
current_line_number = Some(line_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
diags.diagnostics.push(diagnostic);
|
||||
}
|
||||
if let Some(line_number) = current_line_number {
|
||||
diags.line_ranges.push(LineDiagnosticRange {
|
||||
line_number,
|
||||
diagnostic_index_range: start..diags.diagnostics.len(),
|
||||
});
|
||||
}
|
||||
|
||||
diags
|
||||
}
|
||||
|
||||
pub(crate) fn iter_lines(&self) -> LineDiagnosticsIterator<T> {
|
||||
LineDiagnosticsIterator {
|
||||
diagnostics: self.diagnostics.as_slice(),
|
||||
inner: self.line_ranges.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Range delineating diagnostics in [`SortedDiagnostics`] that begin on a single line.
|
||||
#[derive(Debug)]
|
||||
struct LineDiagnosticRange {
|
||||
line_number: OneIndexed,
|
||||
diagnostic_index_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Iterator to group sorted diagnostics by line.
|
||||
pub(crate) struct LineDiagnosticsIterator<'a, T> {
|
||||
diagnostics: &'a [T],
|
||||
inner: std::slice::Iter<'a, LineDiagnosticRange>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
{
|
||||
type Item = LineDiagnostics<'a, T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let LineDiagnosticRange {
|
||||
line_number,
|
||||
diagnostic_index_range,
|
||||
} = self.inner.next()?;
|
||||
Some(LineDiagnostics {
|
||||
line_number: *line_number,
|
||||
diagnostics: &self.diagnostics[diagnostic_index_range.clone()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Clone + Ranged {}
|
||||
|
||||
/// All diagnostics that start on a single line of source code in one embedded Python file.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineDiagnostics<'a, T> {
|
||||
/// Line number on which these diagnostics start.
|
||||
pub(crate) line_number: OneIndexed,
|
||||
|
||||
/// Diagnostics starting on this line.
|
||||
pub(crate) diagnostics: &'a [T],
|
||||
}
|
||||
|
||||
impl<T> Deref for LineDiagnostics<'_, T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiagnosticWithLine<T> {
|
||||
line_number: OneIndexed,
|
||||
diagnostic: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
#[test]
|
||||
fn sort_and_group() {
|
||||
let mut db = Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", "one\ntwo\n").unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
let lines = line_index(&db, file);
|
||||
|
||||
let ranges = vec![
|
||||
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
||||
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
||||
TextRange::new(TextSize::new(1), TextSize::new(7)),
|
||||
];
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(&ranges, &lines);
|
||||
let grouped = sorted.iter_lines().collect::<Vec<_>>();
|
||||
|
||||
let [line1, line2] = &grouped[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(0));
|
||||
assert_eq!(line1.diagnostics.len(), 2);
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
||||
assert_eq!(line2.diagnostics.len(), 1);
|
||||
}
|
||||
}
|
||||
95
crates/red_knot_test/src/lib.rs
Normal file
95
crates/red_knot_test/src/lib.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
type Failures = BTreeMap<SystemPathBuf, matcher::FailuresByLine>;
|
||||
|
||||
mod assertion;
|
||||
mod db;
|
||||
mod diagnostic;
|
||||
mod matcher;
|
||||
mod parser;
|
||||
|
||||
/// Run `path` as a markdown test suite with given `title`.
|
||||
///
|
||||
/// Panic on test failure, and print failure details.
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub fn run(path: &PathBuf, title: &str) {
|
||||
let source = std::fs::read_to_string(path).unwrap();
|
||||
let suite = match test_parser::parse(title, &source) {
|
||||
Ok(suite) => suite,
|
||||
Err(err) => {
|
||||
panic!("Error parsing `{}`: {err}", path.to_str().unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
let mut any_failures = false;
|
||||
for test in suite.tests() {
|
||||
if let Err(failures) = run_test(&test) {
|
||||
any_failures = true;
|
||||
println!("\n{}\n", test.name().bold().underline());
|
||||
|
||||
for (path, by_line) in failures {
|
||||
println!("{}", path.as_str().bold());
|
||||
for (line_number, failures) in by_line.iter() {
|
||||
for failure in failures {
|
||||
let line_info = format!("line {line_number}:").cyan();
|
||||
println!(" {line_info} {failure}");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}\n", "-".repeat(50));
|
||||
|
||||
assert!(!any_failures, "Some tests failed.");
|
||||
}
|
||||
|
||||
fn run_test(test: &parser::MarkdownTest) -> Result<(), Failures> {
|
||||
let workspace_root = SystemPathBuf::from("/src");
|
||||
let mut db = db::Db::setup(workspace_root.clone());
|
||||
|
||||
let mut system_paths = vec![];
|
||||
|
||||
for file in test.files() {
|
||||
assert!(
|
||||
matches!(file.lang, "py" | "pyi"),
|
||||
"Non-Python files not supported yet."
|
||||
);
|
||||
let full_path = workspace_root.join(file.path);
|
||||
db.write_file(&full_path, file.code).unwrap();
|
||||
system_paths.push(full_path);
|
||||
}
|
||||
|
||||
let mut failures = BTreeMap::default();
|
||||
|
||||
for path in system_paths {
|
||||
let file = system_path_to_file(&db, path.clone()).unwrap();
|
||||
let parsed = parsed_module(&db, file);
|
||||
|
||||
// TODO allow testing against code with syntax errors
|
||||
assert!(
|
||||
parsed.errors().is_empty(),
|
||||
"Python syntax errors in {}, {:?}: {:?}",
|
||||
test.name(),
|
||||
path,
|
||||
parsed.errors()
|
||||
);
|
||||
|
||||
matcher::match_file(&db, file, check_types(&db, file)).unwrap_or_else(|line_failures| {
|
||||
failures.insert(path, line_failures);
|
||||
});
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
929
crates/red_knot_test/src/matcher.rs
Normal file
929
crates/red_knot_test/src/matcher.rs
Normal file
@@ -0,0 +1,929 @@
|
||||
//! Match [`TypeCheckDiagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! mismatches.
|
||||
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use red_knot_python_semantic::types::TypeCheckDiagnostic;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct FailuresByLine {
|
||||
failures: Vec<String>,
|
||||
lines: Vec<LineFailures>,
|
||||
}
|
||||
|
||||
impl FailuresByLine {
|
||||
pub(super) fn iter(&self) -> impl Iterator<Item = (OneIndexed, &[String])> {
|
||||
self.lines.iter().map(|line_failures| {
|
||||
(
|
||||
line_failures.line_number,
|
||||
&self.failures[line_failures.range.clone()],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
|
||||
let start = self.failures.len();
|
||||
self.failures.extend(messages);
|
||||
self.lines.push(LineFailures {
|
||||
line_number,
|
||||
range: start..self.failures.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.lines.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LineFailures {
|
||||
line_number: OneIndexed,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
pub(super) fn match_file<T>(
|
||||
db: &Db,
|
||||
file: File,
|
||||
diagnostics: impl IntoIterator<Item = T>,
|
||||
) -> Result<(), FailuresByLine>
|
||||
where
|
||||
T: Diagnostic + Clone,
|
||||
{
|
||||
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
||||
// ordered by line number.
|
||||
let assertions = InlineFileAssertions::from_file(db, file);
|
||||
let diagnostics = SortedDiagnostics::new(diagnostics, &line_index(db, file));
|
||||
|
||||
// Get iterators over assertions and diagnostics grouped by line, in ascending line order.
|
||||
let mut line_assertions = assertions.into_iter();
|
||||
let mut line_diagnostics = diagnostics.iter_lines();
|
||||
|
||||
let mut current_assertions = line_assertions.next();
|
||||
let mut current_diagnostics = line_diagnostics.next();
|
||||
|
||||
let matcher = Matcher::from_file(db, file);
|
||||
let mut failures = FailuresByLine::default();
|
||||
|
||||
loop {
|
||||
match (¤t_assertions, ¤t_diagnostics) {
|
||||
(Some(assertions), Some(diagnostics)) => {
|
||||
match assertions.line_number.cmp(&diagnostics.line_number) {
|
||||
Ordering::Equal => {
|
||||
// We have assertions and diagnostics on the same line; check for
|
||||
// matches and error on any that don't match, then advance both
|
||||
// iterators.
|
||||
matcher
|
||||
.match_line(diagnostics, assertions)
|
||||
.unwrap_or_else(|messages| {
|
||||
failures.push(assertions.line_number, messages);
|
||||
});
|
||||
current_assertions = line_assertions.next();
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
Ordering::Less => {
|
||||
// We have assertions on an earlier line than diagnostics; report these
|
||||
// assertions as all unmatched, and advance the assertions iterator.
|
||||
failures.push(assertions.line_number, unmatched(assertions));
|
||||
current_assertions = line_assertions.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
// We have diagnostics on an earlier line than assertions; report these
|
||||
// diagnostics as all unmatched, and advance the diagnostics iterator.
|
||||
failures.push(diagnostics.line_number, unmatched(diagnostics));
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(assertions), None) => {
|
||||
// We've exhausted diagnostics but still have assertions; report these assertions
|
||||
// as unmatched and advance the assertions iterator.
|
||||
failures.push(assertions.line_number, unmatched(assertions));
|
||||
current_assertions = line_assertions.next();
|
||||
}
|
||||
(None, Some(diagnostics)) => {
|
||||
// We've exhausted assertions but still have diagnostics; report these
|
||||
// diagnostics as unmatched and advance the diagnostics iterator.
|
||||
failures.push(diagnostics.line_number, unmatched(diagnostics));
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
// When we've exhausted both diagnostics and assertions, break.
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait Diagnostic: Ranged {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> &str;
|
||||
}
|
||||
|
||||
impl Diagnostic for Arc<TypeCheckDiagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
self.as_ref().rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.as_ref().message()
|
||||
}
|
||||
}
|
||||
|
||||
trait Unmatched {
|
||||
fn unmatched(&self) -> String;
|
||||
}
|
||||
|
||||
fn unmatched<'a, T: Unmatched + 'a>(unmatched: &'a [T]) -> Vec<String> {
|
||||
unmatched.iter().map(Unmatched::unmatched).collect()
|
||||
}
|
||||
|
||||
trait UnmatchedWithColumn {
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String;
|
||||
}
|
||||
|
||||
impl Unmatched for Assertion<'_> {
|
||||
fn unmatched(&self) -> String {
|
||||
format!("{} {self}", "unmatched assertion:".red())
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
|
||||
diagnostic: &T,
|
||||
original: std::fmt::Arguments,
|
||||
) -> String {
|
||||
if diagnostic.rule() == "undefined-reveal" {
|
||||
format!(
|
||||
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
|
||||
"used built-in `reveal_type`:".yellow()
|
||||
)
|
||||
} else {
|
||||
format!("{} {original}", "unexpected error:".red())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Unmatched for T
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn unmatched(&self) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"[{}] "{}""#, self.rule(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UnmatchedWithColumn for T
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"{column} [{}] "{}""#, self.rule(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Matcher {
|
||||
line_index: LineIndex,
|
||||
source: SourceText,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
fn from_file(db: &Db, file: File) -> Self {
|
||||
Self {
|
||||
line_index: line_index(db, file),
|
||||
source: source_text(db, file),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check a slice of [`Diagnostic`]s against a slice of [`Assertion`]s.
|
||||
///
|
||||
/// Return vector of [`Unmatched`] for any unmatched diagnostics or assertions.
|
||||
fn match_line<'a, 'b, T: Diagnostic + 'a>(
|
||||
&self,
|
||||
diagnostics: &'a [T],
|
||||
assertions: &'a [Assertion<'b>],
|
||||
) -> Result<(), Vec<String>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut failures = vec![];
|
||||
let mut unmatched: Vec<_> = diagnostics.iter().collect();
|
||||
for assertion in assertions {
|
||||
if matches!(
|
||||
assertion,
|
||||
Assertion::Error(ErrorAssertion {
|
||||
rule: None,
|
||||
message_contains: None,
|
||||
..
|
||||
})
|
||||
) {
|
||||
failures.push(format!(
|
||||
"{} no rule or message text",
|
||||
"invalid assertion:".red()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if !self.matches(assertion, &mut unmatched) {
|
||||
failures.push(assertion.unmatched());
|
||||
}
|
||||
}
|
||||
for diagnostic in unmatched {
|
||||
failures.push(diagnostic.unmatched_with_column(self.column(diagnostic)));
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
fn column<T: Ranged>(&self, ranged: &T) -> OneIndexed {
|
||||
self.line_index
|
||||
.source_location(ranged.start(), &self.source)
|
||||
.column
|
||||
}
|
||||
|
||||
/// Check if `assertion` matches any [`Diagnostic`]s in `unmatched`.
|
||||
///
|
||||
/// If so, return `true` and remove the matched diagnostics from `unmatched`. Otherwise, return
|
||||
/// `false`.
|
||||
///
|
||||
/// An `Error` assertion can only match one diagnostic; even if it could match more than one,
|
||||
/// we short-circuit after the first match.
|
||||
///
|
||||
/// A `Revealed` assertion must match a revealed-type diagnostic, and may also match an
|
||||
/// undefined-reveal diagnostic, if present.
|
||||
fn matches<T: Diagnostic>(&self, assertion: &Assertion, unmatched: &mut Vec<&T>) -> bool {
|
||||
match assertion {
|
||||
Assertion::Error(error) => {
|
||||
let position = unmatched.iter().position(|diagnostic| {
|
||||
!error.rule.is_some_and(|rule| rule != diagnostic.rule())
|
||||
&& !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
&& !error
|
||||
.message_contains
|
||||
.is_some_and(|needle| !diagnostic.message().contains(needle))
|
||||
});
|
||||
if let Some(position) = position {
|
||||
unmatched.swap_remove(position);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Assertion::Revealed(expected_type) => {
|
||||
let mut matched_revealed_type = None;
|
||||
let mut matched_undefined_reveal = None;
|
||||
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
|
||||
for (index, diagnostic) in unmatched.iter().enumerate() {
|
||||
if matched_revealed_type.is_none()
|
||||
&& diagnostic.rule() == "revealed-type"
|
||||
&& diagnostic.message() == expected_reveal_type_message
|
||||
{
|
||||
matched_revealed_type = Some(index);
|
||||
} else if matched_undefined_reveal.is_none()
|
||||
&& diagnostic.rule() == "undefined-reveal"
|
||||
{
|
||||
matched_undefined_reveal = Some(index);
|
||||
}
|
||||
if matched_revealed_type.is_some() && matched_undefined_reveal.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut idx = 0;
|
||||
unmatched.retain(|_| {
|
||||
let retain =
|
||||
Some(idx) != matched_revealed_type && Some(idx) != matched_undefined_reveal;
|
||||
idx += 1;
|
||||
retain
|
||||
});
|
||||
matched_revealed_type.is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl TestDiagnostic {
|
||||
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
|
||||
let offset: u32 = offset.try_into().unwrap();
|
||||
Self {
|
||||
rule,
|
||||
message,
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TestDiagnostic {
|
||||
fn range(&self) -> ruff_text_size::TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
fn get_result(source: &str, diagnostics: Vec<TestDiagnostic>) -> Result<(), FailuresByLine> {
|
||||
colored::control::set_override(false);
|
||||
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
|
||||
super::match_file(&db, file, diagnostics)
|
||||
}
|
||||
|
||||
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {
|
||||
let Err(failures) = result else {
|
||||
panic!("expected a failure");
|
||||
};
|
||||
|
||||
let expected: Vec<(OneIndexed, Vec<String>)> = messages
|
||||
.iter()
|
||||
.map(|(idx, msgs)| {
|
||||
(
|
||||
OneIndexed::from_zero_indexed(*idx),
|
||||
msgs.iter().map(ToString::to_string).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let failures: Vec<(OneIndexed, Vec<String>)> = failures
|
||||
.iter()
|
||||
.map(|(idx, msgs)| (idx, msgs.to_vec()))
|
||||
.collect();
|
||||
|
||||
assert_eq!(failures, expected);
|
||||
}
|
||||
|
||||
fn assert_ok(result: &Result<(), FailuresByLine>) {
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_wrong_message() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("revealed-type", "Something else", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [revealed-type] "Something else""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_unmatched() {
|
||||
let result = get_result("x # revealed: Foo", vec![]);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match_with_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match_with_only_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0)],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_mismatch_with_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [revealed-type] "Revealed type is `Bar`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type_unmatched() {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type_mismatched() {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [something-else]",
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_unmatched() {
|
||||
let result = get_result("x # error: [rule]", vec![]);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: error: [rule]"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [some-rule]",
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: "contains this""#,
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_column_and_rule() {
|
||||
let result = get_result(
|
||||
"x # error: 1 [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![TestDiagnostic::new("rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: 2 [rule]",
|
||||
r#"unexpected error: 1 [rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_column_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_rule_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_column() {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_rule() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new(
|
||||
"other-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [other-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interspersed_matches_and_mismatches() {
|
||||
let source = dedent(
|
||||
r#"
|
||||
1 # error: [line-one]
|
||||
2
|
||||
3 # error: [line-three]
|
||||
4 # error: [line-four]
|
||||
5
|
||||
6: # error: [line-six]
|
||||
"#,
|
||||
);
|
||||
let two = source.find('2').unwrap();
|
||||
let three = source.find('3').unwrap();
|
||||
let five = source.find('5').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
TestDiagnostic::new("line-three", "msg", three),
|
||||
TestDiagnostic::new("line-five", "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[
|
||||
(1, &["unmatched assertion: error: [line-one]"]),
|
||||
(2, &[r#"unexpected error: [line-two] "msg""#]),
|
||||
(4, &["unmatched assertion: error: [line-four]"]),
|
||||
(5, &[r#"unexpected error: [line-five] "msg""#]),
|
||||
(6, &["unmatched assertion: error: [line-six]"]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_diagnostics_than_assertions() {
|
||||
let source = dedent(
|
||||
r#"
|
||||
1 # error: [line-one]
|
||||
2
|
||||
"#,
|
||||
);
|
||||
let one = source.find('1').unwrap();
|
||||
let two = source.find('2').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-one", "msg", one),
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [other-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line_all_same() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [one-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line_mismatch() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [other-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
TestDiagnostic::new("third-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parenthesized_expression() {
|
||||
let source = dedent(
|
||||
"
|
||||
a = b + (
|
||||
error: [undefined-reveal]
|
||||
reveal_type(5) # revealed: Literal[5]
|
||||
)
|
||||
",
|
||||
);
|
||||
let reveal = source.find("reveal_type").unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_error_assertion_not_allowed() {
|
||||
let source = "x # error:";
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_only_error_assertion_not_allowed() {
|
||||
let source = "x # error: 1";
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
}
|
||||
576
crates/red_knot_test/src/parser.rs
Normal file
576
crates/red_knot_test/src/parser.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
/// Parse the Markdown `source` as a test suite with given `title`.
|
||||
pub(crate) fn parse<'s>(title: &'s str, source: &'s str) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
let parser = Parser::new(title, source);
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
/// A parsed markdown file containing tests.
|
||||
///
|
||||
/// Borrows from the source string and filepath it was created from.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTestSuite<'s> {
|
||||
/// Header sections.
|
||||
sections: IndexVec<SectionId, Section<'s>>,
|
||||
|
||||
/// Test files embedded within the Markdown file.
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
}
|
||||
|
||||
impl<'s> MarkdownTestSuite<'s> {
|
||||
pub(crate) fn tests(&self) -> MarkdownTestIterator<'_, 's> {
|
||||
MarkdownTestIterator {
|
||||
suite: self,
|
||||
current_file_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single test inside a [`MarkdownTestSuite`].
|
||||
///
|
||||
/// A test is a single header section (or the implicit root section, if there are no Markdown
|
||||
/// headers in the file), containing one or more embedded Python files as fenced code blocks, and
|
||||
/// containing no nested header subsections.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTest<'m, 's> {
|
||||
suite: &'m MarkdownTestSuite<'s>,
|
||||
section: &'m Section<'s>,
|
||||
files: &'m [EmbeddedFile<'s>],
|
||||
}
|
||||
|
||||
impl<'m, 's> MarkdownTest<'m, 's> {
|
||||
pub(crate) fn name(&self) -> String {
|
||||
let mut name = String::new();
|
||||
let mut parent_id = self.section.parent_id;
|
||||
while let Some(next_id) = parent_id {
|
||||
let parent = &self.suite.sections[next_id];
|
||||
parent_id = parent.parent_id;
|
||||
if !name.is_empty() {
|
||||
name.insert_str(0, " - ");
|
||||
}
|
||||
name.insert_str(0, parent.title);
|
||||
}
|
||||
if !name.is_empty() {
|
||||
name.push_str(" - ");
|
||||
}
|
||||
name.push_str(self.section.title);
|
||||
name
|
||||
}
|
||||
|
||||
pub(crate) fn files(&self) -> impl Iterator<Item = &'m EmbeddedFile<'s>> {
|
||||
self.files.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator yielding all [`MarkdownTest`]s in a [`MarkdownTestSuite`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTestIterator<'m, 's> {
|
||||
suite: &'m MarkdownTestSuite<'s>,
|
||||
current_file_index: usize,
|
||||
}
|
||||
|
||||
impl<'m, 's> Iterator for MarkdownTestIterator<'m, 's> {
|
||||
type Item = MarkdownTest<'m, 's>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut current_file_index = self.current_file_index;
|
||||
let mut file = self.suite.files.get(current_file_index.into());
|
||||
let section_id = file?.section;
|
||||
while file.is_some_and(|file| file.section == section_id) {
|
||||
current_file_index += 1;
|
||||
file = self.suite.files.get(current_file_index.into());
|
||||
}
|
||||
let files = &self.suite.files[EmbeddedFileId::from_usize(self.current_file_index)
|
||||
..EmbeddedFileId::from_usize(current_file_index)];
|
||||
self.current_file_index = current_file_index;
|
||||
Some(MarkdownTest {
|
||||
suite: self.suite,
|
||||
section: &self.suite.sections[section_id],
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct SectionId;
|
||||
|
||||
/// A single header section of a [`MarkdownTestSuite`], or the implicit root "section".
|
||||
///
|
||||
/// A header section is the part of a Markdown file beginning with a `#`-prefixed header line, and
|
||||
/// extending until the next header line at the same or higher outline level (that is, with the
|
||||
/// same number or fewer `#` characters).
|
||||
///
|
||||
/// A header section may either contain one or more embedded Python files (making it a
|
||||
/// [`MarkdownTest`]), or it may contain nested sections (headers with more `#` characters), but
|
||||
/// not both.
|
||||
#[derive(Debug)]
|
||||
struct Section<'s> {
|
||||
title: &'s str,
|
||||
level: u8,
|
||||
parent_id: Option<SectionId>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct EmbeddedFileId;
|
||||
|
||||
/// A single file embedded in a [`Section`] as a fenced code block.
|
||||
///
|
||||
/// Currently must be a Python file (`py` language) or type stub (`pyi`). In the future we plan
|
||||
/// support other kinds of files as well (TOML configuration, typeshed VERSIONS, `pth` files...).
|
||||
///
|
||||
/// A Python embedded file makes its containing [`Section`] into a [`MarkdownTest`], and will be
|
||||
/// type-checked and searched for inline-comment assertions to match against the diagnostics from
|
||||
/// type checking.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EmbeddedFile<'s> {
|
||||
section: SectionId,
|
||||
pub(crate) path: &'s str,
|
||||
pub(crate) lang: &'s str,
|
||||
pub(crate) code: &'s str,
|
||||
}
|
||||
|
||||
/// Matches an arbitrary amount of whitespace (including newlines), followed by a sequence of `#`
|
||||
/// characters, followed by a title heading, followed by a newline.
|
||||
static HEADER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(\s*\n)*(?<level>#+)\s+(?<title>.+)\s*\n").unwrap());
|
||||
|
||||
/// Matches a code block fenced by triple backticks, possibly with language and `key=val`
|
||||
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
||||
static CODE_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"^```(?<lang>\w+)(?<config>( +\S+)*)\s*\n(?<code>(.|\n)*?)\n?```\s*\n").unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SectionStack(Vec<SectionId>);
|
||||
|
||||
impl SectionStack {
|
||||
fn new(root_section_id: SectionId) -> Self {
|
||||
Self(vec![root_section_id])
|
||||
}
|
||||
|
||||
fn push(&mut self, section_id: SectionId) {
|
||||
self.0.push(section_id);
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<SectionId> {
|
||||
let popped = self.0.pop();
|
||||
debug_assert_ne!(popped, None, "Should never pop the implicit root section");
|
||||
debug_assert!(
|
||||
!self.0.is_empty(),
|
||||
"Should never pop the implicit root section"
|
||||
);
|
||||
popped
|
||||
}
|
||||
|
||||
fn parent(&mut self) -> SectionId {
|
||||
*self
|
||||
.0
|
||||
.last()
|
||||
.expect("Should never pop the implicit root section")
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the source of a Markdown file into a [`MarkdownTestSuite`].
|
||||
#[derive(Debug)]
|
||||
struct Parser<'s> {
|
||||
/// [`Section`]s of the final [`MarkdownTestSuite`].
|
||||
sections: IndexVec<SectionId, Section<'s>>,
|
||||
|
||||
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
unparsed: &'s str,
|
||||
|
||||
/// Stack of ancestor sections.
|
||||
stack: SectionStack,
|
||||
|
||||
/// Names of embedded files in current active section.
|
||||
current_section_files: Option<FxHashSet<&'s str>>,
|
||||
}
|
||||
|
||||
impl<'s> Parser<'s> {
|
||||
fn new(title: &'s str, source: &'s str) -> Self {
|
||||
let mut sections = IndexVec::default();
|
||||
let root_section_id = sections.push(Section {
|
||||
title,
|
||||
level: 0,
|
||||
parent_id: None,
|
||||
});
|
||||
Self {
|
||||
sections,
|
||||
files: IndexVec::default(),
|
||||
unparsed: source,
|
||||
stack: SectionStack::new(root_section_id),
|
||||
current_section_files: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(mut self) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
self.parse_impl()?;
|
||||
Ok(self.finish())
|
||||
}
|
||||
|
||||
fn finish(mut self) -> MarkdownTestSuite<'s> {
|
||||
self.sections.shrink_to_fit();
|
||||
self.files.shrink_to_fit();
|
||||
|
||||
MarkdownTestSuite {
|
||||
sections: self.sections,
|
||||
files: self.files,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_impl(&mut self) -> anyhow::Result<()> {
|
||||
while !self.unparsed.is_empty() {
|
||||
if let Some(captures) = self.scan(&HEADER_RE) {
|
||||
self.parse_header(&captures)?;
|
||||
} else if let Some(captures) = self.scan(&CODE_RE) {
|
||||
self.parse_code_block(&captures)?;
|
||||
} else {
|
||||
// ignore other Markdown syntax (paragraphs, etc) used as comments in the test
|
||||
if let Some(next_newline) = self.unparsed.find('\n') {
|
||||
(_, self.unparsed) = self.unparsed.split_at(next_newline + 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_header(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
let header_level = captures["level"].len();
|
||||
self.pop_sections_to_level(header_level);
|
||||
|
||||
let parent = self.stack.parent();
|
||||
|
||||
let section = Section {
|
||||
// HEADER_RE can't match without a match for group 'title'.
|
||||
title: captures.name("title").unwrap().into(),
|
||||
level: header_level.try_into()?,
|
||||
parent_id: Some(parent),
|
||||
};
|
||||
|
||||
if self.current_section_files.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Header '{}' not valid inside a test case; parent '{}' has code files.",
|
||||
section.title,
|
||||
self.sections[parent].title,
|
||||
));
|
||||
}
|
||||
|
||||
let section_id = self.sections.push(section);
|
||||
self.stack.push(section_id);
|
||||
|
||||
self.current_section_files = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_code_block(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
// We never pop the implicit root section.
|
||||
let parent = self.stack.parent();
|
||||
|
||||
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
|
||||
|
||||
if let Some(config_match) = captures.name("config") {
|
||||
for item in config_match.as_str().split_whitespace() {
|
||||
let mut parts = item.split('=');
|
||||
let key = parts.next().unwrap();
|
||||
let Some(val) = parts.next() else {
|
||||
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
|
||||
};
|
||||
if parts.next().is_some() {
|
||||
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
|
||||
}
|
||||
if config.insert(key, val).is_some() {
|
||||
return Err(anyhow::anyhow!("Duplicate config item `{}`.", item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = config.get("path").copied().unwrap_or("test.py");
|
||||
|
||||
self.files.push(EmbeddedFile {
|
||||
path,
|
||||
section: parent,
|
||||
// CODE_RE can't match without matches for 'lang' and 'code'.
|
||||
lang: captures.name("lang").unwrap().into(),
|
||||
code: captures.name("code").unwrap().into(),
|
||||
});
|
||||
|
||||
if let Some(current_files) = &mut self.current_section_files {
|
||||
if !current_files.insert(path) {
|
||||
if path == "test.py" {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`. \
|
||||
(This is the default filename; \
|
||||
consider giving some files an explicit name with `path=...`.)",
|
||||
self.sections[parent].title
|
||||
));
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`.",
|
||||
self.sections[parent].title
|
||||
));
|
||||
};
|
||||
} else {
|
||||
self.current_section_files = Some(FxHashSet::from_iter([path]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pop_sections_to_level(&mut self, level: usize) {
|
||||
while level <= self.sections[self.stack.parent()].level.into() {
|
||||
self.stack.pop();
|
||||
// We would have errored before pushing a child section if there were files, so we know
|
||||
// no parent section can have files.
|
||||
self.current_section_files = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get capture groups and advance cursor past match if unparsed text matches `pattern`.
|
||||
fn scan(&mut self, pattern: &Regex) -> Option<Captures<'s>> {
|
||||
if let Some(captures) = pattern.captures(self.unparsed) {
|
||||
let (_, unparsed) = self.unparsed.split_at(captures.get(0).unwrap().end());
|
||||
self.unparsed = unparsed;
|
||||
Some(captures)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let mf = super::parse("file.md", "").unwrap();
|
||||
|
||||
assert!(mf.tests().next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_file_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
|
||||
assert_eq!(test.name(), "file.md");
|
||||
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_tests() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
# Two
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test1, test2] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected two tests");
|
||||
};
|
||||
|
||||
assert_eq!(test1.name(), "file.md - One");
|
||||
assert_eq!(test2.name(), "file.md - Two");
|
||||
|
||||
let [file] = test1.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
|
||||
let [file] = test2.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "y = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_file_path() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_line_file() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.code, "x = 1\ny = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_header_inside_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
## Two
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Header 'Two' not valid inside a test case; parent 'One' has code files."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_no_equals() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Invalid config item `foo`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_too_many_equals() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar=baz
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Invalid config item `foo=bar=baz`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_duplicate() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar foo=baz
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Duplicate config item `foo=baz`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_name_files_in_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `file.md` has duplicate files named `test.py`. \
|
||||
(This is the default filename; consider giving some files an explicit name \
|
||||
with `path=...`.)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_name_files_in_test_non_default() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py path=foo.py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `file.md` has duplicate files named `foo.py`."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ the project the stubs are for, but instead report them here to typeshed.**
|
||||
Further documentation on stub files, typeshed, and Python's typing system in
|
||||
general, can also be found at https://typing.readthedocs.io/en/latest/.
|
||||
|
||||
Typeshed supports Python versions 3.8 and up.
|
||||
Typeshed supports Python versions 3.8 to 3.13.
|
||||
|
||||
## Using
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
91a58b07cdd807b1d965e04ba85af2adab8bf924
|
||||
a871efd90ca2734b3341dde98cffab66f3e08cee
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
__future__: 3.0-
|
||||
__main__: 3.0-
|
||||
_ast: 3.0-
|
||||
_asyncio: 3.0-
|
||||
_bisect: 3.0-
|
||||
_bootlocale: 3.4-3.9
|
||||
_codecs: 3.0-
|
||||
@@ -37,6 +38,7 @@ _imp: 3.0-
|
||||
_interpchannels: 3.13-
|
||||
_interpqueues: 3.13-
|
||||
_interpreters: 3.13-
|
||||
_io: 3.0-
|
||||
_json: 3.0-
|
||||
_locale: 3.0-
|
||||
_lsprof: 3.0-
|
||||
@@ -50,6 +52,8 @@ _pydecimal: 3.5-
|
||||
_random: 3.0-
|
||||
_sitebuiltins: 3.4-
|
||||
_socket: 3.0- # present in 3.0 at runtime, but not in typeshed
|
||||
_sqlite3: 3.0-
|
||||
_ssl: 3.0-
|
||||
_stat: 3.4-
|
||||
_thread: 3.0-
|
||||
_threading_local: 3.0-
|
||||
|
||||
1811
crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi
vendored
1811
crates/red_knot_vendored/vendor/typeshed/stdlib/_ast.pyi
vendored
File diff suppressed because it is too large
Load Diff
120
crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi
vendored
Normal file
120
crates/red_knot_vendored/vendor/typeshed/stdlib/_asyncio.pyi
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
import sys
|
||||
from asyncio.events import AbstractEventLoop
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Generator, Iterable
|
||||
from contextvars import Context
|
||||
from types import FrameType
|
||||
from typing import Any, Literal, TextIO, TypeVar
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
from types import GenericAlias
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
_TaskYieldType: TypeAlias = Future[object] | None
|
||||
|
||||
class Future(Awaitable[_T], Iterable[_T]):
|
||||
_state: str
|
||||
@property
|
||||
def _exception(self) -> BaseException | None: ...
|
||||
_blocking: bool
|
||||
@property
|
||||
def _log_traceback(self) -> bool: ...
|
||||
@_log_traceback.setter
|
||||
def _log_traceback(self, val: Literal[False]) -> None: ...
|
||||
_asyncio_future_blocking: bool # is a part of duck-typing contract for `Future`
|
||||
def __init__(self, *, loop: AbstractEventLoop | None = ...) -> None: ...
|
||||
def __del__(self) -> None: ...
|
||||
def get_loop(self) -> AbstractEventLoop: ...
|
||||
@property
|
||||
def _callbacks(self) -> list[tuple[Callable[[Self], Any], Context]]: ...
|
||||
def add_done_callback(self, fn: Callable[[Self], object], /, *, context: Context | None = None) -> None: ...
|
||||
if sys.version_info >= (3, 9):
|
||||
def cancel(self, msg: Any | None = None) -> bool: ...
|
||||
else:
|
||||
def cancel(self) -> bool: ...
|
||||
|
||||
def cancelled(self) -> bool: ...
|
||||
def done(self) -> bool: ...
|
||||
def result(self) -> _T: ...
|
||||
def exception(self) -> BaseException | None: ...
|
||||
def remove_done_callback(self, fn: Callable[[Self], object], /) -> int: ...
|
||||
def set_result(self, result: _T, /) -> None: ...
|
||||
def set_exception(self, exception: type | BaseException, /) -> None: ...
|
||||
def __iter__(self) -> Generator[Any, None, _T]: ...
|
||||
def __await__(self) -> Generator[Any, None, _T]: ...
|
||||
@property
|
||||
def _loop(self) -> AbstractEventLoop: ...
|
||||
if sys.version_info >= (3, 9):
|
||||
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
_TaskCompatibleCoro: TypeAlias = Coroutine[Any, Any, _T_co]
|
||||
elif sys.version_info >= (3, 9):
|
||||
_TaskCompatibleCoro: TypeAlias = Generator[_TaskYieldType, None, _T_co] | Coroutine[Any, Any, _T_co]
|
||||
else:
|
||||
_TaskCompatibleCoro: TypeAlias = Generator[_TaskYieldType, None, _T_co] | Awaitable[_T_co]
|
||||
|
||||
# mypy and pyright complain that a subclass of an invariant class shouldn't be covariant.
|
||||
# While this is true in general, here it's sort-of okay to have a covariant subclass,
|
||||
# since the only reason why `asyncio.Future` is invariant is the `set_result()` method,
|
||||
# and `asyncio.Task.set_result()` always raises.
|
||||
class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments]
|
||||
if sys.version_info >= (3, 12):
|
||||
def __init__(
|
||||
self,
|
||||
coro: _TaskCompatibleCoro[_T_co],
|
||||
*,
|
||||
loop: AbstractEventLoop = ...,
|
||||
name: str | None = ...,
|
||||
context: Context | None = None,
|
||||
eager_start: bool = False,
|
||||
) -> None: ...
|
||||
elif sys.version_info >= (3, 11):
|
||||
def __init__(
|
||||
self,
|
||||
coro: _TaskCompatibleCoro[_T_co],
|
||||
*,
|
||||
loop: AbstractEventLoop = ...,
|
||||
name: str | None = ...,
|
||||
context: Context | None = None,
|
||||
) -> None: ...
|
||||
else:
|
||||
def __init__(
|
||||
self, coro: _TaskCompatibleCoro[_T_co], *, loop: AbstractEventLoop = ..., name: str | None = ...
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
def get_coro(self) -> _TaskCompatibleCoro[_T_co] | None: ...
|
||||
else:
|
||||
def get_coro(self) -> _TaskCompatibleCoro[_T_co]: ...
|
||||
|
||||
def get_name(self) -> str: ...
|
||||
def set_name(self, value: object, /) -> None: ...
|
||||
if sys.version_info >= (3, 12):
|
||||
def get_context(self) -> Context: ...
|
||||
|
||||
def get_stack(self, *, limit: int | None = None) -> list[FrameType]: ...
|
||||
def print_stack(self, *, limit: int | None = None, file: TextIO | None = None) -> None: ...
|
||||
if sys.version_info >= (3, 11):
|
||||
def cancelling(self) -> int: ...
|
||||
def uncancel(self) -> int: ...
|
||||
if sys.version_info < (3, 9):
|
||||
@classmethod
|
||||
def current_task(cls, loop: AbstractEventLoop | None = None) -> Task[Any] | None: ...
|
||||
@classmethod
|
||||
def all_tasks(cls, loop: AbstractEventLoop | None = None) -> set[Task[Any]]: ...
|
||||
if sys.version_info >= (3, 9):
|
||||
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
|
||||
def get_event_loop() -> AbstractEventLoop: ...
|
||||
def get_running_loop() -> AbstractEventLoop: ...
|
||||
def _set_running_loop(loop: AbstractEventLoop | None, /) -> None: ...
|
||||
def _get_running_loop() -> AbstractEventLoop: ...
|
||||
def _register_task(task: Task[Any]) -> None: ...
|
||||
def _unregister_task(task: Task[Any]) -> None: ...
|
||||
def _enter_task(loop: AbstractEventLoop, task: Task[Any]) -> None: ...
|
||||
def _leave_task(loop: AbstractEventLoop, task: Task[Any]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
def current_task(loop: AbstractEventLoop | None = None) -> Task[Any] | None: ...
|
||||
@@ -20,6 +20,8 @@ _QuotingType: TypeAlias = int
|
||||
|
||||
class Error(Exception): ...
|
||||
|
||||
_DialectLike: TypeAlias = str | Dialect | type[Dialect]
|
||||
|
||||
class Dialect:
|
||||
delimiter: str
|
||||
quotechar: str | None
|
||||
@@ -29,9 +31,18 @@ class Dialect:
|
||||
lineterminator: str
|
||||
quoting: _QuotingType
|
||||
strict: bool
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
_DialectLike: TypeAlias = str | Dialect | type[Dialect]
|
||||
def __init__(
|
||||
self,
|
||||
dialect: _DialectLike | None = ...,
|
||||
delimiter: str = ",",
|
||||
doublequote: bool = True,
|
||||
escapechar: str | None = None,
|
||||
lineterminator: str = "\r\n",
|
||||
quotechar: str | None = '"',
|
||||
quoting: _QuotingType = 0,
|
||||
skipinitialspace: bool = False,
|
||||
strict: bool = False,
|
||||
) -> None: ...
|
||||
|
||||
class _reader(Iterator[list[str]]):
|
||||
@property
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
from _typeshed import ReadableBuffer, WriteableBuffer
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
||||
from ctypes import CDLL, ArgumentError as ArgumentError
|
||||
from ctypes import CDLL, ArgumentError as ArgumentError, c_void_p
|
||||
from typing import Any, ClassVar, Generic, TypeVar, overload
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
@@ -99,6 +99,9 @@ class _Pointer(_PointerLike, _CData, Generic[_CT]):
|
||||
def __getitem__(self, key: slice, /) -> list[Any]: ...
|
||||
def __setitem__(self, key: int, value: Any, /) -> None: ...
|
||||
|
||||
@overload
|
||||
def POINTER(type: None, /) -> type[c_void_p]: ...
|
||||
@overload
|
||||
def POINTER(type: type[_CT], /) -> type[_Pointer[_CT]]: ...
|
||||
def pointer(obj: _CT, /) -> _Pointer[_CT]: ...
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ if sys.version_info >= (3, 9):
|
||||
|
||||
def getmouse() -> tuple[int, int, int, int, int]: ...
|
||||
def getsyx() -> tuple[int, int]: ...
|
||||
def getwin(file: SupportsRead[bytes], /) -> _CursesWindow: ...
|
||||
def getwin(file: SupportsRead[bytes], /) -> window: ...
|
||||
def halfdelay(tenths: int, /) -> None: ...
|
||||
def has_colors() -> bool: ...
|
||||
|
||||
@@ -310,7 +310,7 @@ def has_il() -> bool: ...
|
||||
def has_key(key: int, /) -> bool: ...
|
||||
def init_color(color_number: int, r: int, g: int, b: int, /) -> None: ...
|
||||
def init_pair(pair_number: int, fg: int, bg: int, /) -> None: ...
|
||||
def initscr() -> _CursesWindow: ...
|
||||
def initscr() -> window: ...
|
||||
def intrflush(flag: bool, /) -> None: ...
|
||||
def is_term_resized(nlines: int, ncols: int, /) -> bool: ...
|
||||
def isendwin() -> bool: ...
|
||||
@@ -321,8 +321,8 @@ def meta(yes: bool, /) -> None: ...
|
||||
def mouseinterval(interval: int, /) -> None: ...
|
||||
def mousemask(newmask: int, /) -> tuple[int, int]: ...
|
||||
def napms(ms: int, /) -> int: ...
|
||||
def newpad(nlines: int, ncols: int, /) -> _CursesWindow: ...
|
||||
def newwin(nlines: int, ncols: int, begin_y: int = ..., begin_x: int = ..., /) -> _CursesWindow: ...
|
||||
def newpad(nlines: int, ncols: int, /) -> window: ...
|
||||
def newwin(nlines: int, ncols: int, begin_y: int = ..., begin_x: int = ..., /) -> window: ...
|
||||
def nl(flag: bool = True, /) -> None: ...
|
||||
def nocbreak() -> None: ...
|
||||
def noecho() -> None: ...
|
||||
@@ -378,7 +378,7 @@ def use_env(flag: bool, /) -> None: ...
|
||||
class error(Exception): ...
|
||||
|
||||
@final
|
||||
class _CursesWindow:
|
||||
class window: # undocumented
|
||||
encoding: str
|
||||
@overload
|
||||
def addch(self, ch: _ChType, attr: int = ...) -> None: ...
|
||||
@@ -431,9 +431,9 @@ class _CursesWindow:
|
||||
def delch(self, y: int, x: int) -> None: ...
|
||||
def deleteln(self) -> None: ...
|
||||
@overload
|
||||
def derwin(self, begin_y: int, begin_x: int) -> _CursesWindow: ...
|
||||
def derwin(self, begin_y: int, begin_x: int) -> window: ...
|
||||
@overload
|
||||
def derwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> _CursesWindow: ...
|
||||
def derwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ...
|
||||
def echochar(self, ch: _ChType, attr: int = ..., /) -> None: ...
|
||||
def enclose(self, y: int, x: int, /) -> bool: ...
|
||||
def erase(self) -> None: ...
|
||||
@@ -505,16 +505,16 @@ class _CursesWindow:
|
||||
@overload
|
||||
def noutrefresh(self, pminrow: int, pmincol: int, sminrow: int, smincol: int, smaxrow: int, smaxcol: int) -> None: ...
|
||||
@overload
|
||||
def overlay(self, destwin: _CursesWindow) -> None: ...
|
||||
def overlay(self, destwin: window) -> None: ...
|
||||
@overload
|
||||
def overlay(
|
||||
self, destwin: _CursesWindow, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int
|
||||
self, destwin: window, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int
|
||||
) -> None: ...
|
||||
@overload
|
||||
def overwrite(self, destwin: _CursesWindow) -> None: ...
|
||||
def overwrite(self, destwin: window) -> None: ...
|
||||
@overload
|
||||
def overwrite(
|
||||
self, destwin: _CursesWindow, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int
|
||||
self, destwin: window, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int
|
||||
) -> None: ...
|
||||
def putwin(self, file: IO[Any], /) -> None: ...
|
||||
def redrawln(self, beg: int, num: int, /) -> None: ...
|
||||
@@ -530,13 +530,13 @@ class _CursesWindow:
|
||||
def standend(self) -> None: ...
|
||||
def standout(self) -> None: ...
|
||||
@overload
|
||||
def subpad(self, begin_y: int, begin_x: int) -> _CursesWindow: ...
|
||||
def subpad(self, begin_y: int, begin_x: int) -> window: ...
|
||||
@overload
|
||||
def subpad(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> _CursesWindow: ...
|
||||
def subpad(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ...
|
||||
@overload
|
||||
def subwin(self, begin_y: int, begin_x: int) -> _CursesWindow: ...
|
||||
def subwin(self, begin_y: int, begin_x: int) -> window: ...
|
||||
@overload
|
||||
def subwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> _CursesWindow: ...
|
||||
def subwin(self, nlines: int, ncols: int, begin_y: int, begin_x: int) -> window: ...
|
||||
def syncdown(self) -> None: ...
|
||||
def syncok(self, flag: bool) -> None: ...
|
||||
def syncup(self) -> None: ...
|
||||
@@ -555,4 +555,3 @@ class _ncurses_version(NamedTuple):
|
||||
patch: int
|
||||
|
||||
ncurses_version: _ncurses_version
|
||||
window = _CursesWindow # undocumented
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
import numbers
|
||||
import sys
|
||||
from collections.abc import Container, Sequence
|
||||
from decimal import (
|
||||
Clamped as Clamped,
|
||||
Context as Context,
|
||||
ConversionSyntax as ConversionSyntax,
|
||||
Decimal as Decimal,
|
||||
DecimalException as DecimalException,
|
||||
DecimalTuple as DecimalTuple,
|
||||
DivisionByZero as DivisionByZero,
|
||||
DivisionImpossible as DivisionImpossible,
|
||||
DivisionUndefined as DivisionUndefined,
|
||||
FloatOperation as FloatOperation,
|
||||
Inexact as Inexact,
|
||||
InvalidContext as InvalidContext,
|
||||
InvalidOperation as InvalidOperation,
|
||||
Overflow as Overflow,
|
||||
Rounded as Rounded,
|
||||
Subnormal as Subnormal,
|
||||
Underflow as Underflow,
|
||||
)
|
||||
from types import TracebackType
|
||||
from typing import Any, ClassVar, Final, Literal, NamedTuple, overload
|
||||
from typing_extensions import Self, TypeAlias
|
||||
from typing import Final
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
_Decimal: TypeAlias = Decimal | int
|
||||
_DecimalNew: TypeAlias = Decimal | float | str | tuple[int, Sequence[int], int]
|
||||
_ComparableNum: TypeAlias = Decimal | float | numbers.Rational
|
||||
_TrapType: TypeAlias = type[DecimalException]
|
||||
|
||||
class _ContextManager:
|
||||
new_context: Context
|
||||
saved_context: Context
|
||||
def __init__(self, new_context: Context) -> None: ...
|
||||
def __enter__(self) -> Context: ...
|
||||
def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ...
|
||||
|
||||
__version__: Final[str]
|
||||
__libmpdec_version__: Final[str]
|
||||
|
||||
class DecimalTuple(NamedTuple):
|
||||
sign: int
|
||||
digits: tuple[int, ...]
|
||||
exponent: int | Literal["n", "N", "F"]
|
||||
|
||||
ROUND_DOWN: Final[str]
|
||||
ROUND_HALF_UP: Final[str]
|
||||
ROUND_HALF_EVEN: Final[str]
|
||||
@@ -32,21 +49,6 @@ MAX_PREC: Final[int]
|
||||
MIN_EMIN: Final[int]
|
||||
MIN_ETINY: Final[int]
|
||||
|
||||
class DecimalException(ArithmeticError): ...
|
||||
class Clamped(DecimalException): ...
|
||||
class InvalidOperation(DecimalException): ...
|
||||
class ConversionSyntax(InvalidOperation): ...
|
||||
class DivisionByZero(DecimalException, ZeroDivisionError): ...
|
||||
class DivisionImpossible(InvalidOperation): ...
|
||||
class DivisionUndefined(InvalidOperation, ZeroDivisionError): ...
|
||||
class Inexact(DecimalException): ...
|
||||
class InvalidContext(InvalidOperation): ...
|
||||
class Rounded(DecimalException): ...
|
||||
class Subnormal(DecimalException): ...
|
||||
class Overflow(Inexact, Rounded): ...
|
||||
class Underflow(Inexact, Rounded, Subnormal): ...
|
||||
class FloatOperation(DecimalException, TypeError): ...
|
||||
|
||||
def setcontext(context: Context, /) -> None: ...
|
||||
def getcontext() -> Context: ...
|
||||
|
||||
@@ -67,215 +69,6 @@ if sys.version_info >= (3, 11):
|
||||
else:
|
||||
def localcontext(ctx: Context | None = None) -> _ContextManager: ...
|
||||
|
||||
class Decimal:
|
||||
def __new__(cls, value: _DecimalNew = ..., context: Context | None = ...) -> Self: ...
|
||||
@classmethod
|
||||
def from_float(cls, f: float, /) -> Self: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def compare(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def __hash__(self) -> int: ...
|
||||
def as_tuple(self) -> DecimalTuple: ...
|
||||
def as_integer_ratio(self) -> tuple[int, int]: ...
|
||||
def to_eng_string(self, context: Context | None = None) -> str: ...
|
||||
def __abs__(self) -> Decimal: ...
|
||||
def __add__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __divmod__(self, value: _Decimal, /) -> tuple[Decimal, Decimal]: ...
|
||||
def __eq__(self, value: object, /) -> bool: ...
|
||||
def __floordiv__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __ge__(self, value: _ComparableNum, /) -> bool: ...
|
||||
def __gt__(self, value: _ComparableNum, /) -> bool: ...
|
||||
def __le__(self, value: _ComparableNum, /) -> bool: ...
|
||||
def __lt__(self, value: _ComparableNum, /) -> bool: ...
|
||||
def __mod__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __mul__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __neg__(self) -> Decimal: ...
|
||||
def __pos__(self) -> Decimal: ...
|
||||
def __pow__(self, value: _Decimal, mod: _Decimal | None = None, /) -> Decimal: ...
|
||||
def __radd__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __rdivmod__(self, value: _Decimal, /) -> tuple[Decimal, Decimal]: ...
|
||||
def __rfloordiv__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __rmod__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __rmul__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __rsub__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __rtruediv__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __sub__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def __truediv__(self, value: _Decimal, /) -> Decimal: ...
|
||||
def remainder_near(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def __float__(self) -> float: ...
|
||||
def __int__(self) -> int: ...
|
||||
def __trunc__(self) -> int: ...
|
||||
@property
|
||||
def real(self) -> Decimal: ...
|
||||
@property
|
||||
def imag(self) -> Decimal: ...
|
||||
def conjugate(self) -> Decimal: ...
|
||||
def __complex__(self) -> complex: ...
|
||||
@overload
|
||||
def __round__(self) -> int: ...
|
||||
@overload
|
||||
def __round__(self, ndigits: int, /) -> Decimal: ...
|
||||
def __floor__(self) -> int: ...
|
||||
def __ceil__(self) -> int: ...
|
||||
def fma(self, other: _Decimal, third: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def __rpow__(self, value: _Decimal, mod: Context | None = None, /) -> Decimal: ...
|
||||
def normalize(self, context: Context | None = None) -> Decimal: ...
|
||||
def quantize(self, exp: _Decimal, rounding: str | None = None, context: Context | None = None) -> Decimal: ...
|
||||
def same_quantum(self, other: _Decimal, context: Context | None = None) -> bool: ...
|
||||
def to_integral_exact(self, rounding: str | None = None, context: Context | None = None) -> Decimal: ...
|
||||
def to_integral_value(self, rounding: str | None = None, context: Context | None = None) -> Decimal: ...
|
||||
def to_integral(self, rounding: str | None = None, context: Context | None = None) -> Decimal: ...
|
||||
def sqrt(self, context: Context | None = None) -> Decimal: ...
|
||||
def max(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def min(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def adjusted(self) -> int: ...
|
||||
def canonical(self) -> Decimal: ...
|
||||
def compare_signal(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def compare_total(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def compare_total_mag(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def copy_abs(self) -> Decimal: ...
|
||||
def copy_negate(self) -> Decimal: ...
|
||||
def copy_sign(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def exp(self, context: Context | None = None) -> Decimal: ...
|
||||
def is_canonical(self) -> bool: ...
|
||||
def is_finite(self) -> bool: ...
|
||||
def is_infinite(self) -> bool: ...
|
||||
def is_nan(self) -> bool: ...
|
||||
def is_normal(self, context: Context | None = None) -> bool: ...
|
||||
def is_qnan(self) -> bool: ...
|
||||
def is_signed(self) -> bool: ...
|
||||
def is_snan(self) -> bool: ...
|
||||
def is_subnormal(self, context: Context | None = None) -> bool: ...
|
||||
def is_zero(self) -> bool: ...
|
||||
def ln(self, context: Context | None = None) -> Decimal: ...
|
||||
def log10(self, context: Context | None = None) -> Decimal: ...
|
||||
def logb(self, context: Context | None = None) -> Decimal: ...
|
||||
def logical_and(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def logical_invert(self, context: Context | None = None) -> Decimal: ...
|
||||
def logical_or(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def logical_xor(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def max_mag(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def min_mag(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def next_minus(self, context: Context | None = None) -> Decimal: ...
|
||||
def next_plus(self, context: Context | None = None) -> Decimal: ...
|
||||
def next_toward(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def number_class(self, context: Context | None = None) -> str: ...
|
||||
def radix(self) -> Decimal: ...
|
||||
def rotate(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def scaleb(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def shift(self, other: _Decimal, context: Context | None = None) -> Decimal: ...
|
||||
def __reduce__(self) -> tuple[type[Self], tuple[str]]: ...
|
||||
def __copy__(self) -> Self: ...
|
||||
def __deepcopy__(self, memo: Any, /) -> Self: ...
|
||||
def __format__(self, specifier: str, context: Context | None = ..., /) -> str: ...
|
||||
|
||||
class _ContextManager:
|
||||
new_context: Context
|
||||
saved_context: Context
|
||||
def __init__(self, new_context: Context) -> None: ...
|
||||
def __enter__(self) -> Context: ...
|
||||
def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ...
|
||||
|
||||
_TrapType: TypeAlias = type[DecimalException]
|
||||
|
||||
class Context:
|
||||
# TODO: Context doesn't allow you to delete *any* attributes from instances of the class at runtime,
|
||||
# even settable attributes like `prec` and `rounding`,
|
||||
# but that's inexpressable in the stub.
|
||||
# Type checkers either ignore it or misinterpret it
|
||||
# if you add a `def __delattr__(self, name: str, /) -> NoReturn` method to the stub
|
||||
prec: int
|
||||
rounding: str
|
||||
Emin: int
|
||||
Emax: int
|
||||
capitals: int
|
||||
clamp: int
|
||||
traps: dict[_TrapType, bool]
|
||||
flags: dict[_TrapType, bool]
|
||||
def __init__(
|
||||
self,
|
||||
prec: int | None = ...,
|
||||
rounding: str | None = ...,
|
||||
Emin: int | None = ...,
|
||||
Emax: int | None = ...,
|
||||
capitals: int | None = ...,
|
||||
clamp: int | None = ...,
|
||||
flags: None | dict[_TrapType, bool] | Container[_TrapType] = ...,
|
||||
traps: None | dict[_TrapType, bool] | Container[_TrapType] = ...,
|
||||
_ignored_flags: list[_TrapType] | None = ...,
|
||||
) -> None: ...
|
||||
def __reduce__(self) -> tuple[type[Self], tuple[Any, ...]]: ...
|
||||
def clear_flags(self) -> None: ...
|
||||
def clear_traps(self) -> None: ...
|
||||
def copy(self) -> Context: ...
|
||||
def __copy__(self) -> Context: ...
|
||||
# see https://github.com/python/cpython/issues/94107
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def Etiny(self) -> int: ...
|
||||
def Etop(self) -> int: ...
|
||||
def create_decimal(self, num: _DecimalNew = "0", /) -> Decimal: ...
|
||||
def create_decimal_from_float(self, f: float, /) -> Decimal: ...
|
||||
def abs(self, x: _Decimal, /) -> Decimal: ...
|
||||
def add(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def canonical(self, x: Decimal, /) -> Decimal: ...
|
||||
def compare(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def compare_signal(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def compare_total(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def compare_total_mag(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def copy_abs(self, x: _Decimal, /) -> Decimal: ...
|
||||
def copy_decimal(self, x: _Decimal, /) -> Decimal: ...
|
||||
def copy_negate(self, x: _Decimal, /) -> Decimal: ...
|
||||
def copy_sign(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def divide(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def divide_int(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def divmod(self, x: _Decimal, y: _Decimal, /) -> tuple[Decimal, Decimal]: ...
|
||||
def exp(self, x: _Decimal, /) -> Decimal: ...
|
||||
def fma(self, x: _Decimal, y: _Decimal, z: _Decimal, /) -> Decimal: ...
|
||||
def is_canonical(self, x: _Decimal, /) -> bool: ...
|
||||
def is_finite(self, x: _Decimal, /) -> bool: ...
|
||||
def is_infinite(self, x: _Decimal, /) -> bool: ...
|
||||
def is_nan(self, x: _Decimal, /) -> bool: ...
|
||||
def is_normal(self, x: _Decimal, /) -> bool: ...
|
||||
def is_qnan(self, x: _Decimal, /) -> bool: ...
|
||||
def is_signed(self, x: _Decimal, /) -> bool: ...
|
||||
def is_snan(self, x: _Decimal, /) -> bool: ...
|
||||
def is_subnormal(self, x: _Decimal, /) -> bool: ...
|
||||
def is_zero(self, x: _Decimal, /) -> bool: ...
|
||||
def ln(self, x: _Decimal, /) -> Decimal: ...
|
||||
def log10(self, x: _Decimal, /) -> Decimal: ...
|
||||
def logb(self, x: _Decimal, /) -> Decimal: ...
|
||||
def logical_and(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def logical_invert(self, x: _Decimal, /) -> Decimal: ...
|
||||
def logical_or(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def logical_xor(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def max(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def max_mag(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def min(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def min_mag(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def minus(self, x: _Decimal, /) -> Decimal: ...
|
||||
def multiply(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def next_minus(self, x: _Decimal, /) -> Decimal: ...
|
||||
def next_plus(self, x: _Decimal, /) -> Decimal: ...
|
||||
def next_toward(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def normalize(self, x: _Decimal, /) -> Decimal: ...
|
||||
def number_class(self, x: _Decimal, /) -> str: ...
|
||||
def plus(self, x: _Decimal, /) -> Decimal: ...
|
||||
def power(self, a: _Decimal, b: _Decimal, modulo: _Decimal | None = None) -> Decimal: ...
|
||||
def quantize(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def radix(self) -> Decimal: ...
|
||||
def remainder(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def remainder_near(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def rotate(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def same_quantum(self, x: _Decimal, y: _Decimal, /) -> bool: ...
|
||||
def scaleb(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def shift(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def sqrt(self, x: _Decimal, /) -> Decimal: ...
|
||||
def subtract(self, x: _Decimal, y: _Decimal, /) -> Decimal: ...
|
||||
def to_eng_string(self, x: _Decimal, /) -> str: ...
|
||||
def to_sci_string(self, x: _Decimal, /) -> str: ...
|
||||
def to_integral_exact(self, x: _Decimal, /) -> Decimal: ...
|
||||
def to_integral_value(self, x: _Decimal, /) -> Decimal: ...
|
||||
def to_integral(self, x: _Decimal, /) -> Decimal: ...
|
||||
|
||||
DefaultContext: Context
|
||||
BasicContext: Context
|
||||
ExtendedContext: Context
|
||||
|
||||
195
crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi
vendored
Normal file
195
crates/red_knot_vendored/vendor/typeshed/stdlib/_io.pyi
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
import builtins
|
||||
import codecs
|
||||
import sys
|
||||
from _typeshed import FileDescriptorOrPath, MaybeNone, ReadableBuffer, WriteableBuffer
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
from io import BufferedIOBase, RawIOBase, TextIOBase, UnsupportedOperation as UnsupportedOperation
|
||||
from os import _Opener
|
||||
from types import TracebackType
|
||||
from typing import IO, Any, BinaryIO, Final, Generic, Literal, Protocol, TextIO, TypeVar, overload, type_check_only
|
||||
from typing_extensions import Self
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
DEFAULT_BUFFER_SIZE: Final = 8192
|
||||
|
||||
open = builtins.open
|
||||
|
||||
def open_code(path: str) -> IO[bytes]: ...
|
||||
|
||||
BlockingIOError = builtins.BlockingIOError
|
||||
|
||||
class _IOBase:
|
||||
def __iter__(self) -> Iterator[bytes]: ...
|
||||
def __next__(self) -> bytes: ...
|
||||
def __enter__(self) -> Self: ...
|
||||
def __exit__(
|
||||
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
|
||||
) -> None: ...
|
||||
def close(self) -> None: ...
|
||||
def fileno(self) -> int: ...
|
||||
def flush(self) -> None: ...
|
||||
def isatty(self) -> bool: ...
|
||||
def readable(self) -> bool: ...
|
||||
read: Callable[..., Any]
|
||||
def readlines(self, hint: int = -1, /) -> list[bytes]: ...
|
||||
def seek(self, offset: int, whence: int = ..., /) -> int: ...
|
||||
def seekable(self) -> bool: ...
|
||||
def tell(self) -> int: ...
|
||||
def truncate(self, size: int | None = ..., /) -> int: ...
|
||||
def writable(self) -> bool: ...
|
||||
write: Callable[..., Any]
|
||||
def writelines(self, lines: Iterable[ReadableBuffer], /) -> None: ...
|
||||
def readline(self, size: int | None = -1, /) -> bytes: ...
|
||||
def __del__(self) -> None: ...
|
||||
@property
|
||||
def closed(self) -> bool: ...
|
||||
def _checkClosed(self) -> None: ... # undocumented
|
||||
|
||||
class _RawIOBase(_IOBase):
|
||||
def readall(self) -> bytes: ...
|
||||
# The following methods can return None if the file is in non-blocking mode
|
||||
# and no data is available.
|
||||
def readinto(self, buffer: WriteableBuffer, /) -> int | MaybeNone: ...
|
||||
def write(self, b: ReadableBuffer, /) -> int | MaybeNone: ...
|
||||
def read(self, size: int = -1, /) -> bytes | MaybeNone: ...
|
||||
|
||||
class _BufferedIOBase(_IOBase):
|
||||
def detach(self) -> RawIOBase: ...
|
||||
def readinto(self, buffer: WriteableBuffer, /) -> int: ...
|
||||
def write(self, buffer: ReadableBuffer, /) -> int: ...
|
||||
def readinto1(self, buffer: WriteableBuffer, /) -> int: ...
|
||||
def read(self, size: int | None = ..., /) -> bytes: ...
|
||||
def read1(self, size: int = ..., /) -> bytes: ...
|
||||
|
||||
class FileIO(RawIOBase, _RawIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of writelines in the base classes
|
||||
mode: str
|
||||
# The type of "name" equals the argument passed in to the constructor,
|
||||
# but that can make FileIO incompatible with other I/O types that assume
|
||||
# "name" is a str. In the future, making FileIO generic might help.
|
||||
name: Any
|
||||
def __init__(
|
||||
self, file: FileDescriptorOrPath, mode: str = ..., closefd: bool = ..., opener: _Opener | None = ...
|
||||
) -> None: ...
|
||||
@property
|
||||
def closefd(self) -> bool: ...
|
||||
|
||||
class BytesIO(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes
|
||||
def __init__(self, initial_bytes: ReadableBuffer = ...) -> None: ...
|
||||
# BytesIO does not contain a "name" field. This workaround is necessary
|
||||
# to allow BytesIO sub-classes to add this field, as it is defined
|
||||
# as a read-only property on IO[].
|
||||
name: Any
|
||||
def getvalue(self) -> bytes: ...
|
||||
def getbuffer(self) -> memoryview: ...
|
||||
def read1(self, size: int | None = -1, /) -> bytes: ...
|
||||
|
||||
class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes
|
||||
raw: RawIOBase
|
||||
def __init__(self, raw: RawIOBase, buffer_size: int = ...) -> None: ...
|
||||
def peek(self, size: int = 0, /) -> bytes: ...
|
||||
|
||||
class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of writelines in the base classes
|
||||
raw: RawIOBase
|
||||
def __init__(self, raw: RawIOBase, buffer_size: int = ...) -> None: ...
|
||||
def write(self, buffer: ReadableBuffer, /) -> int: ...
|
||||
|
||||
class BufferedRandom(BufferedReader, BufferedWriter, BufferedIOBase, _BufferedIOBase): # type: ignore[misc] # incompatible definitions of methods in the base classes
|
||||
def seek(self, target: int, whence: int = 0, /) -> int: ... # stubtest needs this
|
||||
|
||||
class BufferedRWPair(BufferedIOBase, _BufferedIOBase):
|
||||
def __init__(self, reader: RawIOBase, writer: RawIOBase, buffer_size: int = ...) -> None: ...
|
||||
def peek(self, size: int = ..., /) -> bytes: ...
|
||||
|
||||
class _TextIOBase(_IOBase):
|
||||
encoding: str
|
||||
errors: str | None
|
||||
newlines: str | tuple[str, ...] | None
|
||||
def __iter__(self) -> Iterator[str]: ... # type: ignore[override]
|
||||
def __next__(self) -> str: ... # type: ignore[override]
|
||||
def detach(self) -> BinaryIO: ...
|
||||
def write(self, s: str, /) -> int: ...
|
||||
def writelines(self, lines: Iterable[str], /) -> None: ... # type: ignore[override]
|
||||
def readline(self, size: int = ..., /) -> str: ... # type: ignore[override]
|
||||
def readlines(self, hint: int = -1, /) -> list[str]: ... # type: ignore[override]
|
||||
def read(self, size: int | None = ..., /) -> str: ...
|
||||
|
||||
@type_check_only
|
||||
class _WrappedBuffer(Protocol):
|
||||
# "name" is wrapped by TextIOWrapper. Its type is inconsistent between
|
||||
# the various I/O types, see the comments on TextIOWrapper.name and
|
||||
# TextIO.name.
|
||||
@property
|
||||
def name(self) -> Any: ...
|
||||
@property
|
||||
def closed(self) -> bool: ...
|
||||
def read(self, size: int = ..., /) -> ReadableBuffer: ...
|
||||
# Optional: def read1(self, size: int, /) -> ReadableBuffer: ...
|
||||
def write(self, b: bytes, /) -> object: ...
|
||||
def flush(self) -> object: ...
|
||||
def close(self) -> object: ...
|
||||
def seekable(self) -> bool: ...
|
||||
def readable(self) -> bool: ...
|
||||
def writable(self) -> bool: ...
|
||||
def truncate(self, size: int, /) -> int: ...
|
||||
def fileno(self) -> int: ...
|
||||
def isatty(self) -> bool: ...
|
||||
# Optional: Only needs to be present if seekable() returns True.
|
||||
# def seek(self, offset: Literal[0], whence: Literal[2]) -> int: ...
|
||||
# def tell(self) -> int: ...
|
||||
|
||||
_BufferT_co = TypeVar("_BufferT_co", bound=_WrappedBuffer, default=_WrappedBuffer, covariant=True)
|
||||
|
||||
class TextIOWrapper(TextIOBase, _TextIOBase, TextIO, Generic[_BufferT_co]): # type: ignore[misc] # incompatible definitions of write in the base classes
|
||||
def __init__(
|
||||
self,
|
||||
buffer: _BufferT_co,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
line_buffering: bool = False,
|
||||
write_through: bool = False,
|
||||
) -> None: ...
|
||||
# Equals the "buffer" argument passed in to the constructor.
|
||||
@property
|
||||
def buffer(self) -> _BufferT_co: ... # type: ignore[override]
|
||||
@property
|
||||
def line_buffering(self) -> bool: ...
|
||||
@property
|
||||
def write_through(self) -> bool: ...
|
||||
def reconfigure(
|
||||
self,
|
||||
*,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
line_buffering: bool | None = None,
|
||||
write_through: bool | None = None,
|
||||
) -> None: ...
|
||||
def readline(self, size: int = -1, /) -> str: ... # type: ignore[override]
|
||||
# Equals the "buffer" argument passed in to the constructor.
|
||||
def detach(self) -> _BufferT_co: ... # type: ignore[override]
|
||||
# TextIOWrapper's version of seek only supports a limited subset of
|
||||
# operations.
|
||||
def seek(self, cookie: int, whence: int = 0, /) -> int: ...
|
||||
|
||||
class StringIO(TextIOWrapper, TextIOBase, _TextIOBase): # type: ignore[misc] # incompatible definitions of write in the base classes
|
||||
def __init__(self, initial_value: str | None = ..., newline: str | None = ...) -> None: ...
|
||||
# StringIO does not contain a "name" field. This workaround is necessary
|
||||
# to allow StringIO sub-classes to add this field, as it is defined
|
||||
# as a read-only property on IO[].
|
||||
name: Any
|
||||
def getvalue(self) -> str: ...
|
||||
|
||||
class IncrementalNewlineDecoder(codecs.IncrementalDecoder):
|
||||
def __init__(self, decoder: codecs.IncrementalDecoder | None, translate: bool, errors: str = ...) -> None: ...
|
||||
def decode(self, input: ReadableBuffer | str, final: bool = False) -> str: ...
|
||||
@property
|
||||
def newlines(self) -> str | tuple[str, ...] | None: ...
|
||||
def setstate(self, state: tuple[bytes, int], /) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
@overload
|
||||
def text_encoding(encoding: None, stacklevel: int = 2, /) -> Literal["locale", "utf-8"]: ...
|
||||
@overload
|
||||
def text_encoding(encoding: _T, stacklevel: int = 2, /) -> _T: ...
|
||||
@@ -1,18 +1,16 @@
|
||||
import sys
|
||||
from _typeshed import SupportsGetItem
|
||||
from collections.abc import Callable, Container, Iterable, MutableMapping, MutableSequence, Sequence
|
||||
from typing import Any, AnyStr, Generic, Protocol, SupportsAbs, SupportsIndex, TypeVar, final, overload
|
||||
from typing_extensions import ParamSpec, TypeAlias, TypeIs, TypeVarTuple, Unpack
|
||||
from operator import attrgetter as attrgetter, itemgetter as itemgetter, methodcaller as methodcaller
|
||||
from typing import Any, AnyStr, Protocol, SupportsAbs, SupportsIndex, TypeVar, overload
|
||||
from typing_extensions import ParamSpec, TypeAlias, TypeIs
|
||||
|
||||
_R = TypeVar("_R")
|
||||
_T = TypeVar("_T")
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
_T1 = TypeVar("_T1")
|
||||
_T2 = TypeVar("_T2")
|
||||
_K = TypeVar("_K")
|
||||
_V = TypeVar("_V")
|
||||
_P = ParamSpec("_P")
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
# The following protocols return "Any" instead of bool, since the comparison
|
||||
# operators can be overloaded to return an arbitrary object. For example,
|
||||
@@ -92,40 +90,6 @@ def setitem(a: MutableSequence[_T], b: slice, c: Sequence[_T], /) -> None: ...
|
||||
@overload
|
||||
def setitem(a: MutableMapping[_K, _V], b: _K, c: _V, /) -> None: ...
|
||||
def length_hint(obj: object, default: int = 0, /) -> int: ...
|
||||
@final
|
||||
class attrgetter(Generic[_T_co]):
|
||||
@overload
|
||||
def __new__(cls, attr: str, /) -> attrgetter[Any]: ...
|
||||
@overload
|
||||
def __new__(cls, attr: str, attr2: str, /) -> attrgetter[tuple[Any, Any]]: ...
|
||||
@overload
|
||||
def __new__(cls, attr: str, attr2: str, attr3: str, /) -> attrgetter[tuple[Any, Any, Any]]: ...
|
||||
@overload
|
||||
def __new__(cls, attr: str, attr2: str, attr3: str, attr4: str, /) -> attrgetter[tuple[Any, Any, Any, Any]]: ...
|
||||
@overload
|
||||
def __new__(cls, attr: str, /, *attrs: str) -> attrgetter[tuple[Any, ...]]: ...
|
||||
def __call__(self, obj: Any, /) -> _T_co: ...
|
||||
|
||||
@final
|
||||
class itemgetter(Generic[_T_co]):
|
||||
@overload
|
||||
def __new__(cls, item: _T, /) -> itemgetter[_T]: ...
|
||||
@overload
|
||||
def __new__(cls, item1: _T1, item2: _T2, /, *items: Unpack[_Ts]) -> itemgetter[tuple[_T1, _T2, Unpack[_Ts]]]: ...
|
||||
# __key: _KT_contra in SupportsGetItem seems to be causing variance issues, ie:
|
||||
# TypeVar "_KT_contra@SupportsGetItem" is contravariant
|
||||
# "tuple[int, int]" is incompatible with protocol "SupportsIndex"
|
||||
# preventing [_T_co, ...] instead of [Any, ...]
|
||||
#
|
||||
# A suspected mypy issue prevents using [..., _T] instead of [..., Any] here.
|
||||
# https://github.com/python/mypy/issues/14032
|
||||
def __call__(self, obj: SupportsGetItem[Any, Any]) -> Any: ...
|
||||
|
||||
@final
|
||||
class methodcaller:
|
||||
def __init__(self, name: str, /, *args: Any, **kwargs: Any) -> None: ...
|
||||
def __call__(self, obj: Any) -> Any: ...
|
||||
|
||||
def iadd(a: Any, b: Any, /) -> Any: ...
|
||||
def iand(a: Any, b: Any, /) -> Any: ...
|
||||
def iconcat(a: Any, b: Any, /) -> Any: ...
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
from _typeshed import ReadableBuffer, WriteableBuffer
|
||||
from collections.abc import Iterable
|
||||
from socket import error as error, gaierror as gaierror, herror as herror, timeout as timeout
|
||||
from typing import Any, SupportsIndex, overload
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
@@ -666,18 +667,6 @@ if sys.platform != "win32":
|
||||
if sys.platform != "win32" and sys.platform != "darwin":
|
||||
IPX_TYPE: int
|
||||
|
||||
# ===== Exceptions =====
|
||||
|
||||
error = OSError
|
||||
|
||||
class herror(error): ...
|
||||
class gaierror(error): ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
timeout = TimeoutError
|
||||
else:
|
||||
class timeout(error): ...
|
||||
|
||||
# ===== Classes =====
|
||||
|
||||
class socket:
|
||||
@@ -687,8 +676,9 @@ class socket:
|
||||
def type(self) -> int: ...
|
||||
@property
|
||||
def proto(self) -> int: ...
|
||||
# F811: "Redefinition of unused `timeout`"
|
||||
@property
|
||||
def timeout(self) -> float | None: ...
|
||||
def timeout(self) -> float | None: ... # noqa: F811
|
||||
if sys.platform == "win32":
|
||||
def __init__(
|
||||
self, family: int = ..., type: int = ..., proto: int = ..., fileno: SupportsIndex | bytes | None = ...
|
||||
@@ -788,7 +778,9 @@ def inet_ntoa(packed_ip: ReadableBuffer, /) -> str: ...
|
||||
def inet_pton(address_family: int, ip_string: str, /) -> bytes: ...
|
||||
def inet_ntop(address_family: int, packed_ip: ReadableBuffer, /) -> str: ...
|
||||
def getdefaulttimeout() -> float | None: ...
|
||||
def setdefaulttimeout(timeout: float | None, /) -> None: ...
|
||||
|
||||
# F811: "Redefinition of unused `timeout`"
|
||||
def setdefaulttimeout(timeout: float | None, /) -> None: ... # noqa: F811
|
||||
|
||||
if sys.platform != "win32":
|
||||
def sethostname(name: str, /) -> None: ...
|
||||
|
||||
312
crates/red_knot_vendored/vendor/typeshed/stdlib/_sqlite3.pyi
vendored
Normal file
312
crates/red_knot_vendored/vendor/typeshed/stdlib/_sqlite3.pyi
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
import sys
|
||||
from _typeshed import ReadableBuffer, StrOrBytesPath
|
||||
from collections.abc import Callable
|
||||
from sqlite3 import (
|
||||
Connection as Connection,
|
||||
Cursor as Cursor,
|
||||
DatabaseError as DatabaseError,
|
||||
DataError as DataError,
|
||||
Error as Error,
|
||||
IntegrityError as IntegrityError,
|
||||
InterfaceError as InterfaceError,
|
||||
InternalError as InternalError,
|
||||
NotSupportedError as NotSupportedError,
|
||||
OperationalError as OperationalError,
|
||||
PrepareProtocol as PrepareProtocol,
|
||||
ProgrammingError as ProgrammingError,
|
||||
Row as Row,
|
||||
Warning as Warning,
|
||||
)
|
||||
from typing import Any, Final, Literal, TypeVar, overload
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from sqlite3 import Blob as Blob
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_ConnectionT = TypeVar("_ConnectionT", bound=Connection)
|
||||
_SqliteData: TypeAlias = str | ReadableBuffer | int | float | None
|
||||
_Adapter: TypeAlias = Callable[[_T], _SqliteData]
|
||||
_Converter: TypeAlias = Callable[[bytes], Any]
|
||||
|
||||
PARSE_COLNAMES: Final[int]
|
||||
PARSE_DECLTYPES: Final[int]
|
||||
SQLITE_ALTER_TABLE: Final[int]
|
||||
SQLITE_ANALYZE: Final[int]
|
||||
SQLITE_ATTACH: Final[int]
|
||||
SQLITE_CREATE_INDEX: Final[int]
|
||||
SQLITE_CREATE_TABLE: Final[int]
|
||||
SQLITE_CREATE_TEMP_INDEX: Final[int]
|
||||
SQLITE_CREATE_TEMP_TABLE: Final[int]
|
||||
SQLITE_CREATE_TEMP_TRIGGER: Final[int]
|
||||
SQLITE_CREATE_TEMP_VIEW: Final[int]
|
||||
SQLITE_CREATE_TRIGGER: Final[int]
|
||||
SQLITE_CREATE_VIEW: Final[int]
|
||||
SQLITE_CREATE_VTABLE: Final[int]
|
||||
SQLITE_DELETE: Final[int]
|
||||
SQLITE_DENY: Final[int]
|
||||
SQLITE_DETACH: Final[int]
|
||||
SQLITE_DONE: Final[int]
|
||||
SQLITE_DROP_INDEX: Final[int]
|
||||
SQLITE_DROP_TABLE: Final[int]
|
||||
SQLITE_DROP_TEMP_INDEX: Final[int]
|
||||
SQLITE_DROP_TEMP_TABLE: Final[int]
|
||||
SQLITE_DROP_TEMP_TRIGGER: Final[int]
|
||||
SQLITE_DROP_TEMP_VIEW: Final[int]
|
||||
SQLITE_DROP_TRIGGER: Final[int]
|
||||
SQLITE_DROP_VIEW: Final[int]
|
||||
SQLITE_DROP_VTABLE: Final[int]
|
||||
SQLITE_FUNCTION: Final[int]
|
||||
SQLITE_IGNORE: Final[int]
|
||||
SQLITE_INSERT: Final[int]
|
||||
SQLITE_OK: Final[int]
|
||||
SQLITE_PRAGMA: Final[int]
|
||||
SQLITE_READ: Final[int]
|
||||
SQLITE_RECURSIVE: Final[int]
|
||||
SQLITE_REINDEX: Final[int]
|
||||
SQLITE_SAVEPOINT: Final[int]
|
||||
SQLITE_SELECT: Final[int]
|
||||
SQLITE_TRANSACTION: Final[int]
|
||||
SQLITE_UPDATE: Final[int]
|
||||
adapters: dict[tuple[type[Any], type[Any]], _Adapter[Any]]
|
||||
converters: dict[str, _Converter]
|
||||
sqlite_version: str
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
version: str
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
LEGACY_TRANSACTION_CONTROL: Final[int]
|
||||
SQLITE_DBCONFIG_DEFENSIVE: Final[int]
|
||||
SQLITE_DBCONFIG_DQS_DDL: Final[int]
|
||||
SQLITE_DBCONFIG_DQS_DML: Final[int]
|
||||
SQLITE_DBCONFIG_ENABLE_FKEY: Final[int]
|
||||
SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER: Final[int]
|
||||
SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: Final[int]
|
||||
SQLITE_DBCONFIG_ENABLE_QPSG: Final[int]
|
||||
SQLITE_DBCONFIG_ENABLE_TRIGGER: Final[int]
|
||||
SQLITE_DBCONFIG_ENABLE_VIEW: Final[int]
|
||||
SQLITE_DBCONFIG_LEGACY_ALTER_TABLE: Final[int]
|
||||
SQLITE_DBCONFIG_LEGACY_FILE_FORMAT: Final[int]
|
||||
SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE: Final[int]
|
||||
SQLITE_DBCONFIG_RESET_DATABASE: Final[int]
|
||||
SQLITE_DBCONFIG_TRIGGER_EQP: Final[int]
|
||||
SQLITE_DBCONFIG_TRUSTED_SCHEMA: Final[int]
|
||||
SQLITE_DBCONFIG_WRITABLE_SCHEMA: Final[int]
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
SQLITE_ABORT: Final[int]
|
||||
SQLITE_ABORT_ROLLBACK: Final[int]
|
||||
SQLITE_AUTH: Final[int]
|
||||
SQLITE_AUTH_USER: Final[int]
|
||||
SQLITE_BUSY: Final[int]
|
||||
SQLITE_BUSY_RECOVERY: Final[int]
|
||||
SQLITE_BUSY_SNAPSHOT: Final[int]
|
||||
SQLITE_BUSY_TIMEOUT: Final[int]
|
||||
SQLITE_CANTOPEN: Final[int]
|
||||
SQLITE_CANTOPEN_CONVPATH: Final[int]
|
||||
SQLITE_CANTOPEN_DIRTYWAL: Final[int]
|
||||
SQLITE_CANTOPEN_FULLPATH: Final[int]
|
||||
SQLITE_CANTOPEN_ISDIR: Final[int]
|
||||
SQLITE_CANTOPEN_NOTEMPDIR: Final[int]
|
||||
SQLITE_CANTOPEN_SYMLINK: Final[int]
|
||||
SQLITE_CONSTRAINT: Final[int]
|
||||
SQLITE_CONSTRAINT_CHECK: Final[int]
|
||||
SQLITE_CONSTRAINT_COMMITHOOK: Final[int]
|
||||
SQLITE_CONSTRAINT_FOREIGNKEY: Final[int]
|
||||
SQLITE_CONSTRAINT_FUNCTION: Final[int]
|
||||
SQLITE_CONSTRAINT_NOTNULL: Final[int]
|
||||
SQLITE_CONSTRAINT_PINNED: Final[int]
|
||||
SQLITE_CONSTRAINT_PRIMARYKEY: Final[int]
|
||||
SQLITE_CONSTRAINT_ROWID: Final[int]
|
||||
SQLITE_CONSTRAINT_TRIGGER: Final[int]
|
||||
SQLITE_CONSTRAINT_UNIQUE: Final[int]
|
||||
SQLITE_CONSTRAINT_VTAB: Final[int]
|
||||
SQLITE_CORRUPT: Final[int]
|
||||
SQLITE_CORRUPT_INDEX: Final[int]
|
||||
SQLITE_CORRUPT_SEQUENCE: Final[int]
|
||||
SQLITE_CORRUPT_VTAB: Final[int]
|
||||
SQLITE_EMPTY: Final[int]
|
||||
SQLITE_ERROR: Final[int]
|
||||
SQLITE_ERROR_MISSING_COLLSEQ: Final[int]
|
||||
SQLITE_ERROR_RETRY: Final[int]
|
||||
SQLITE_ERROR_SNAPSHOT: Final[int]
|
||||
SQLITE_FORMAT: Final[int]
|
||||
SQLITE_FULL: Final[int]
|
||||
SQLITE_INTERNAL: Final[int]
|
||||
SQLITE_INTERRUPT: Final[int]
|
||||
SQLITE_IOERR: Final[int]
|
||||
SQLITE_IOERR_ACCESS: Final[int]
|
||||
SQLITE_IOERR_AUTH: Final[int]
|
||||
SQLITE_IOERR_BEGIN_ATOMIC: Final[int]
|
||||
SQLITE_IOERR_BLOCKED: Final[int]
|
||||
SQLITE_IOERR_CHECKRESERVEDLOCK: Final[int]
|
||||
SQLITE_IOERR_CLOSE: Final[int]
|
||||
SQLITE_IOERR_COMMIT_ATOMIC: Final[int]
|
||||
SQLITE_IOERR_CONVPATH: Final[int]
|
||||
SQLITE_IOERR_CORRUPTFS: Final[int]
|
||||
SQLITE_IOERR_DATA: Final[int]
|
||||
SQLITE_IOERR_DELETE: Final[int]
|
||||
SQLITE_IOERR_DELETE_NOENT: Final[int]
|
||||
SQLITE_IOERR_DIR_CLOSE: Final[int]
|
||||
SQLITE_IOERR_DIR_FSYNC: Final[int]
|
||||
SQLITE_IOERR_FSTAT: Final[int]
|
||||
SQLITE_IOERR_FSYNC: Final[int]
|
||||
SQLITE_IOERR_GETTEMPPATH: Final[int]
|
||||
SQLITE_IOERR_LOCK: Final[int]
|
||||
SQLITE_IOERR_MMAP: Final[int]
|
||||
SQLITE_IOERR_NOMEM: Final[int]
|
||||
SQLITE_IOERR_RDLOCK: Final[int]
|
||||
SQLITE_IOERR_READ: Final[int]
|
||||
SQLITE_IOERR_ROLLBACK_ATOMIC: Final[int]
|
||||
SQLITE_IOERR_SEEK: Final[int]
|
||||
SQLITE_IOERR_SHMLOCK: Final[int]
|
||||
SQLITE_IOERR_SHMMAP: Final[int]
|
||||
SQLITE_IOERR_SHMOPEN: Final[int]
|
||||
SQLITE_IOERR_SHMSIZE: Final[int]
|
||||
SQLITE_IOERR_SHORT_READ: Final[int]
|
||||
SQLITE_IOERR_TRUNCATE: Final[int]
|
||||
SQLITE_IOERR_UNLOCK: Final[int]
|
||||
SQLITE_IOERR_VNODE: Final[int]
|
||||
SQLITE_IOERR_WRITE: Final[int]
|
||||
SQLITE_LIMIT_ATTACHED: Final[int]
|
||||
SQLITE_LIMIT_COLUMN: Final[int]
|
||||
SQLITE_LIMIT_COMPOUND_SELECT: Final[int]
|
||||
SQLITE_LIMIT_EXPR_DEPTH: Final[int]
|
||||
SQLITE_LIMIT_FUNCTION_ARG: Final[int]
|
||||
SQLITE_LIMIT_LENGTH: Final[int]
|
||||
SQLITE_LIMIT_LIKE_PATTERN_LENGTH: Final[int]
|
||||
SQLITE_LIMIT_SQL_LENGTH: Final[int]
|
||||
SQLITE_LIMIT_TRIGGER_DEPTH: Final[int]
|
||||
SQLITE_LIMIT_VARIABLE_NUMBER: Final[int]
|
||||
SQLITE_LIMIT_VDBE_OP: Final[int]
|
||||
SQLITE_LIMIT_WORKER_THREADS: Final[int]
|
||||
SQLITE_LOCKED: Final[int]
|
||||
SQLITE_LOCKED_SHAREDCACHE: Final[int]
|
||||
SQLITE_LOCKED_VTAB: Final[int]
|
||||
SQLITE_MISMATCH: Final[int]
|
||||
SQLITE_MISUSE: Final[int]
|
||||
SQLITE_NOLFS: Final[int]
|
||||
SQLITE_NOMEM: Final[int]
|
||||
SQLITE_NOTADB: Final[int]
|
||||
SQLITE_NOTFOUND: Final[int]
|
||||
SQLITE_NOTICE: Final[int]
|
||||
SQLITE_NOTICE_RECOVER_ROLLBACK: Final[int]
|
||||
SQLITE_NOTICE_RECOVER_WAL: Final[int]
|
||||
SQLITE_OK_LOAD_PERMANENTLY: Final[int]
|
||||
SQLITE_OK_SYMLINK: Final[int]
|
||||
SQLITE_PERM: Final[int]
|
||||
SQLITE_PROTOCOL: Final[int]
|
||||
SQLITE_RANGE: Final[int]
|
||||
SQLITE_READONLY: Final[int]
|
||||
SQLITE_READONLY_CANTINIT: Final[int]
|
||||
SQLITE_READONLY_CANTLOCK: Final[int]
|
||||
SQLITE_READONLY_DBMOVED: Final[int]
|
||||
SQLITE_READONLY_DIRECTORY: Final[int]
|
||||
SQLITE_READONLY_RECOVERY: Final[int]
|
||||
SQLITE_READONLY_ROLLBACK: Final[int]
|
||||
SQLITE_ROW: Final[int]
|
||||
SQLITE_SCHEMA: Final[int]
|
||||
SQLITE_TOOBIG: Final[int]
|
||||
SQLITE_WARNING: Final[int]
|
||||
SQLITE_WARNING_AUTOINDEX: Final[int]
|
||||
threadsafety: Final[int]
|
||||
|
||||
# Can take or return anything depending on what's in the registry.
|
||||
@overload
|
||||
def adapt(obj: Any, proto: Any, /) -> Any: ...
|
||||
@overload
|
||||
def adapt(obj: Any, proto: Any, alt: _T, /) -> Any | _T: ...
|
||||
def complete_statement(statement: str) -> bool: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
@overload
|
||||
def connect(
|
||||
database: StrOrBytesPath,
|
||||
timeout: float = 5.0,
|
||||
detect_types: int = 0,
|
||||
isolation_level: Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None = "DEFERRED",
|
||||
check_same_thread: bool = True,
|
||||
cached_statements: int = 128,
|
||||
uri: bool = False,
|
||||
*,
|
||||
autocommit: bool = ...,
|
||||
) -> Connection: ...
|
||||
@overload
|
||||
def connect(
|
||||
database: StrOrBytesPath,
|
||||
timeout: float,
|
||||
detect_types: int,
|
||||
isolation_level: Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None,
|
||||
check_same_thread: bool,
|
||||
factory: type[_ConnectionT],
|
||||
cached_statements: int = 128,
|
||||
uri: bool = False,
|
||||
*,
|
||||
autocommit: bool = ...,
|
||||
) -> _ConnectionT: ...
|
||||
@overload
|
||||
def connect(
|
||||
database: StrOrBytesPath,
|
||||
timeout: float = 5.0,
|
||||
detect_types: int = 0,
|
||||
isolation_level: Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None = "DEFERRED",
|
||||
check_same_thread: bool = True,
|
||||
*,
|
||||
factory: type[_ConnectionT],
|
||||
cached_statements: int = 128,
|
||||
uri: bool = False,
|
||||
autocommit: bool = ...,
|
||||
) -> _ConnectionT: ...
|
||||
|
||||
else:
|
||||
@overload
|
||||
def connect(
|
||||
database: StrOrBytesPath,
|
||||
timeout: float = 5.0,
|
||||
detect_types: int = 0,
|
||||
isolation_level: Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None = "DEFERRED",
|
||||
check_same_thread: bool = True,
|
||||
cached_statements: int = 128,
|
||||
uri: bool = False,
|
||||
) -> Connection: ...
|
||||
@overload
|
||||
def connect(
|
||||
database: StrOrBytesPath,
|
||||
timeout: float,
|
||||
detect_types: int,
|
||||
isolation_level: Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None,
|
||||
check_same_thread: bool,
|
||||
factory: type[_ConnectionT],
|
||||
cached_statements: int = 128,
|
||||
uri: bool = False,
|
||||
) -> _ConnectionT: ...
|
||||
@overload
|
||||
def connect(
|
||||
database: StrOrBytesPath,
|
||||
timeout: float = 5.0,
|
||||
detect_types: int = 0,
|
||||
isolation_level: Literal["DEFERRED", "EXCLUSIVE", "IMMEDIATE"] | None = "DEFERRED",
|
||||
check_same_thread: bool = True,
|
||||
*,
|
||||
factory: type[_ConnectionT],
|
||||
cached_statements: int = 128,
|
||||
uri: bool = False,
|
||||
) -> _ConnectionT: ...
|
||||
|
||||
def enable_callback_tracebacks(enable: bool, /) -> None: ...
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# takes a pos-or-keyword argument because there is a C wrapper
|
||||
def enable_shared_cache(do_enable: int) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
def register_adapter(type: type[_T], adapter: _Adapter[_T], /) -> None: ...
|
||||
def register_converter(typename: str, converter: _Converter, /) -> None: ...
|
||||
|
||||
else:
|
||||
def register_adapter(type: type[_T], caster: _Adapter[_T], /) -> None: ...
|
||||
def register_converter(name: str, converter: _Converter, /) -> None: ...
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
OptimizedUnicode = str
|
||||
292
crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi
vendored
Normal file
292
crates/red_knot_vendored/vendor/typeshed/stdlib/_ssl.pyi
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
import sys
|
||||
from _typeshed import ReadableBuffer, StrOrBytesPath
|
||||
from collections.abc import Callable
|
||||
from ssl import (
|
||||
SSLCertVerificationError as SSLCertVerificationError,
|
||||
SSLContext,
|
||||
SSLEOFError as SSLEOFError,
|
||||
SSLError as SSLError,
|
||||
SSLObject,
|
||||
SSLSyscallError as SSLSyscallError,
|
||||
SSLWantReadError as SSLWantReadError,
|
||||
SSLWantWriteError as SSLWantWriteError,
|
||||
SSLZeroReturnError as SSLZeroReturnError,
|
||||
)
|
||||
from typing import Any, Literal, TypedDict, final, overload
|
||||
from typing_extensions import NotRequired, Self, TypeAlias
|
||||
|
||||
_PasswordType: TypeAlias = Callable[[], str | bytes | bytearray] | str | bytes | bytearray
|
||||
_PCTRTT: TypeAlias = tuple[tuple[str, str], ...]
|
||||
_PCTRTTT: TypeAlias = tuple[_PCTRTT, ...]
|
||||
_PeerCertRetDictType: TypeAlias = dict[str, str | _PCTRTTT | _PCTRTT]
|
||||
|
||||
class _Cipher(TypedDict):
|
||||
aead: bool
|
||||
alg_bits: int
|
||||
auth: str
|
||||
description: str
|
||||
digest: str | None
|
||||
id: int
|
||||
kea: str
|
||||
name: str
|
||||
protocol: str
|
||||
strength_bits: int
|
||||
symmetric: str
|
||||
|
||||
class _CertInfo(TypedDict):
|
||||
subject: tuple[tuple[tuple[str, str], ...], ...]
|
||||
issuer: tuple[tuple[tuple[str, str], ...], ...]
|
||||
version: int
|
||||
serialNumber: str
|
||||
notBefore: str
|
||||
notAfter: str
|
||||
subjectAltName: NotRequired[tuple[tuple[str, str], ...] | None]
|
||||
OCSP: NotRequired[tuple[str, ...] | None]
|
||||
caIssuers: NotRequired[tuple[str, ...] | None]
|
||||
crlDistributionPoints: NotRequired[tuple[str, ...] | None]
|
||||
|
||||
def RAND_add(string: str | ReadableBuffer, entropy: float, /) -> None: ...
|
||||
def RAND_bytes(n: int, /) -> bytes: ...
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
def RAND_pseudo_bytes(n: int, /) -> tuple[bytes, bool]: ...
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
def RAND_egd(path: str) -> None: ...
|
||||
|
||||
def RAND_status() -> bool: ...
|
||||
def get_default_verify_paths() -> tuple[str, str, str, str]: ...
|
||||
|
||||
if sys.platform == "win32":
|
||||
_EnumRetType: TypeAlias = list[tuple[bytes, str, set[str] | bool]]
|
||||
def enum_certificates(store_name: str) -> _EnumRetType: ...
|
||||
def enum_crls(store_name: str) -> _EnumRetType: ...
|
||||
|
||||
def txt2obj(txt: str, name: bool = False) -> tuple[int, str, str, str]: ...
|
||||
def nid2obj(nid: int, /) -> tuple[int, str, str, str]: ...
|
||||
|
||||
class _SSLContext:
|
||||
check_hostname: bool
|
||||
keylog_filename: str | None
|
||||
maximum_version: int
|
||||
minimum_version: int
|
||||
num_tickets: int
|
||||
options: int
|
||||
post_handshake_auth: bool
|
||||
protocol: int
|
||||
if sys.version_info >= (3, 10):
|
||||
security_level: int
|
||||
sni_callback: Callable[[SSLObject, str, SSLContext], None | int] | None
|
||||
verify_flags: int
|
||||
verify_mode: int
|
||||
def __new__(cls, protocol: int, /) -> Self: ...
|
||||
def cert_store_stats(self) -> dict[str, int]: ...
|
||||
@overload
|
||||
def get_ca_certs(self, binary_form: Literal[False] = False) -> list[_PeerCertRetDictType]: ...
|
||||
@overload
|
||||
def get_ca_certs(self, binary_form: Literal[True]) -> list[bytes]: ...
|
||||
@overload
|
||||
def get_ca_certs(self, binary_form: bool = False) -> Any: ...
|
||||
def get_ciphers(self) -> list[_Cipher]: ...
|
||||
def load_cert_chain(
|
||||
self, certfile: StrOrBytesPath, keyfile: StrOrBytesPath | None = None, password: _PasswordType | None = None
|
||||
) -> None: ...
|
||||
def load_dh_params(self, path: str, /) -> None: ...
|
||||
def load_verify_locations(
|
||||
self,
|
||||
cafile: StrOrBytesPath | None = None,
|
||||
capath: StrOrBytesPath | None = None,
|
||||
cadata: str | ReadableBuffer | None = None,
|
||||
) -> None: ...
|
||||
def session_stats(self) -> dict[str, int]: ...
|
||||
def set_ciphers(self, cipherlist: str, /) -> None: ...
|
||||
def set_default_verify_paths(self) -> None: ...
|
||||
def set_ecdh_curve(self, name: str, /) -> None: ...
|
||||
if sys.version_info >= (3, 13):
|
||||
def set_psk_client_callback(self, callback: Callable[[str | None], tuple[str | None, bytes]] | None) -> None: ...
|
||||
def set_psk_server_callback(
|
||||
self, callback: Callable[[str | None], tuple[str | None, bytes]] | None, identity_hint: str | None = None
|
||||
) -> None: ...
|
||||
|
||||
@final
|
||||
class MemoryBIO:
|
||||
eof: bool
|
||||
pending: int
|
||||
def __new__(self) -> Self: ...
|
||||
def read(self, size: int = -1, /) -> bytes: ...
|
||||
def write(self, b: ReadableBuffer, /) -> int: ...
|
||||
def write_eof(self) -> None: ...
|
||||
|
||||
@final
|
||||
class SSLSession:
|
||||
@property
|
||||
def has_ticket(self) -> bool: ...
|
||||
@property
|
||||
def id(self) -> bytes: ...
|
||||
@property
|
||||
def ticket_lifetime_hint(self) -> int: ...
|
||||
@property
|
||||
def time(self) -> int: ...
|
||||
@property
|
||||
def timeout(self) -> int: ...
|
||||
|
||||
# _ssl.Certificate is weird: it can't be instantiated or subclassed.
|
||||
# Instances can only be created via methods of the private _ssl._SSLSocket class,
|
||||
# for which the relevant method signatures are:
|
||||
#
|
||||
# class _SSLSocket:
|
||||
# def get_unverified_chain(self) -> list[Certificate] | None: ...
|
||||
# def get_verified_chain(self) -> list[Certificate] | None: ...
|
||||
#
|
||||
# You can find a _ssl._SSLSocket object as the _sslobj attribute of a ssl.SSLSocket object
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
@final
|
||||
class Certificate:
|
||||
def get_info(self) -> _CertInfo: ...
|
||||
@overload
|
||||
def public_bytes(self) -> str: ...
|
||||
@overload
|
||||
def public_bytes(self, format: Literal[1] = 1, /) -> str: ... # ENCODING_PEM
|
||||
@overload
|
||||
def public_bytes(self, format: Literal[2], /) -> bytes: ... # ENCODING_DER
|
||||
@overload
|
||||
def public_bytes(self, format: int, /) -> str | bytes: ...
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
err_codes_to_names: dict[tuple[int, int], str]
|
||||
err_names_to_codes: dict[str, tuple[int, int]]
|
||||
lib_codes_to_names: dict[int, str]
|
||||
|
||||
_DEFAULT_CIPHERS: str
|
||||
|
||||
# SSL error numbers
|
||||
SSL_ERROR_ZERO_RETURN: int
|
||||
SSL_ERROR_WANT_READ: int
|
||||
SSL_ERROR_WANT_WRITE: int
|
||||
SSL_ERROR_WANT_X509_LOOKUP: int
|
||||
SSL_ERROR_SYSCALL: int
|
||||
SSL_ERROR_SSL: int
|
||||
SSL_ERROR_WANT_CONNECT: int
|
||||
SSL_ERROR_EOF: int
|
||||
SSL_ERROR_INVALID_ERROR_CODE: int
|
||||
|
||||
# verify modes
|
||||
CERT_NONE: int
|
||||
CERT_OPTIONAL: int
|
||||
CERT_REQUIRED: int
|
||||
|
||||
# verify flags
|
||||
VERIFY_DEFAULT: int
|
||||
VERIFY_CRL_CHECK_LEAF: int
|
||||
VERIFY_CRL_CHECK_CHAIN: int
|
||||
VERIFY_X509_STRICT: int
|
||||
VERIFY_X509_TRUSTED_FIRST: int
|
||||
if sys.version_info >= (3, 10):
|
||||
VERIFY_ALLOW_PROXY_CERTS: int
|
||||
VERIFY_X509_PARTIAL_CHAIN: int
|
||||
|
||||
# alert descriptions
|
||||
ALERT_DESCRIPTION_CLOSE_NOTIFY: int
|
||||
ALERT_DESCRIPTION_UNEXPECTED_MESSAGE: int
|
||||
ALERT_DESCRIPTION_BAD_RECORD_MAC: int
|
||||
ALERT_DESCRIPTION_RECORD_OVERFLOW: int
|
||||
ALERT_DESCRIPTION_DECOMPRESSION_FAILURE: int
|
||||
ALERT_DESCRIPTION_HANDSHAKE_FAILURE: int
|
||||
ALERT_DESCRIPTION_BAD_CERTIFICATE: int
|
||||
ALERT_DESCRIPTION_UNSUPPORTED_CERTIFICATE: int
|
||||
ALERT_DESCRIPTION_CERTIFICATE_REVOKED: int
|
||||
ALERT_DESCRIPTION_CERTIFICATE_EXPIRED: int
|
||||
ALERT_DESCRIPTION_CERTIFICATE_UNKNOWN: int
|
||||
ALERT_DESCRIPTION_ILLEGAL_PARAMETER: int
|
||||
ALERT_DESCRIPTION_UNKNOWN_CA: int
|
||||
ALERT_DESCRIPTION_ACCESS_DENIED: int
|
||||
ALERT_DESCRIPTION_DECODE_ERROR: int
|
||||
ALERT_DESCRIPTION_DECRYPT_ERROR: int
|
||||
ALERT_DESCRIPTION_PROTOCOL_VERSION: int
|
||||
ALERT_DESCRIPTION_INSUFFICIENT_SECURITY: int
|
||||
ALERT_DESCRIPTION_INTERNAL_ERROR: int
|
||||
ALERT_DESCRIPTION_USER_CANCELLED: int
|
||||
ALERT_DESCRIPTION_NO_RENEGOTIATION: int
|
||||
ALERT_DESCRIPTION_UNSUPPORTED_EXTENSION: int
|
||||
ALERT_DESCRIPTION_CERTIFICATE_UNOBTAINABLE: int
|
||||
ALERT_DESCRIPTION_UNRECOGNIZED_NAME: int
|
||||
ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE: int
|
||||
ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE: int
|
||||
ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY: int
|
||||
|
||||
# protocol versions
|
||||
PROTOCOL_SSLv23: int
|
||||
PROTOCOL_TLS: int
|
||||
PROTOCOL_TLS_CLIENT: int
|
||||
PROTOCOL_TLS_SERVER: int
|
||||
PROTOCOL_TLSv1: int
|
||||
PROTOCOL_TLSv1_1: int
|
||||
PROTOCOL_TLSv1_2: int
|
||||
|
||||
# protocol options
|
||||
OP_ALL: int
|
||||
OP_NO_SSLv2: int
|
||||
OP_NO_SSLv3: int
|
||||
OP_NO_TLSv1: int
|
||||
OP_NO_TLSv1_1: int
|
||||
OP_NO_TLSv1_2: int
|
||||
OP_NO_TLSv1_3: int
|
||||
OP_CIPHER_SERVER_PREFERENCE: int
|
||||
OP_SINGLE_DH_USE: int
|
||||
OP_NO_TICKET: int
|
||||
OP_SINGLE_ECDH_USE: int
|
||||
OP_NO_COMPRESSION: int
|
||||
OP_ENABLE_MIDDLEBOX_COMPAT: int
|
||||
OP_NO_RENEGOTIATION: int
|
||||
if sys.version_info >= (3, 11):
|
||||
OP_IGNORE_UNEXPECTED_EOF: int
|
||||
elif sys.version_info >= (3, 8) and sys.platform == "linux":
|
||||
OP_IGNORE_UNEXPECTED_EOF: int
|
||||
if sys.version_info >= (3, 12):
|
||||
OP_LEGACY_SERVER_CONNECT: int
|
||||
OP_ENABLE_KTLS: int
|
||||
|
||||
# host flags
|
||||
HOSTFLAG_ALWAYS_CHECK_SUBJECT: int
|
||||
HOSTFLAG_NEVER_CHECK_SUBJECT: int
|
||||
HOSTFLAG_NO_WILDCARDS: int
|
||||
HOSTFLAG_NO_PARTIAL_WILDCARDS: int
|
||||
HOSTFLAG_MULTI_LABEL_WILDCARDS: int
|
||||
HOSTFLAG_SINGLE_LABEL_SUBDOMAINS: int
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
# certificate file types
|
||||
# Typed as Literal so the overload on Certificate.public_bytes can work properly.
|
||||
ENCODING_PEM: Literal[1]
|
||||
ENCODING_DER: Literal[2]
|
||||
|
||||
# protocol versions
|
||||
PROTO_MINIMUM_SUPPORTED: int
|
||||
PROTO_MAXIMUM_SUPPORTED: int
|
||||
PROTO_SSLv3: int
|
||||
PROTO_TLSv1: int
|
||||
PROTO_TLSv1_1: int
|
||||
PROTO_TLSv1_2: int
|
||||
PROTO_TLSv1_3: int
|
||||
|
||||
# feature support
|
||||
HAS_SNI: bool
|
||||
HAS_TLS_UNIQUE: bool
|
||||
HAS_ECDH: bool
|
||||
HAS_NPN: bool
|
||||
if sys.version_info >= (3, 13):
|
||||
HAS_PSK: bool
|
||||
HAS_ALPN: bool
|
||||
HAS_SSLv2: bool
|
||||
HAS_SSLv3: bool
|
||||
HAS_TLSv1: bool
|
||||
HAS_TLSv1_1: bool
|
||||
HAS_TLSv1_2: bool
|
||||
HAS_TLSv1_3: bool
|
||||
|
||||
# version info
|
||||
OPENSSL_VERSION_NUMBER: int
|
||||
OPENSSL_VERSION_INFO: tuple[int, int, int, int, int]
|
||||
OPENSSL_VERSION: str
|
||||
_OPENSSL_API_VERSION: tuple[int, int, int, int, int]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user