Compare commits
71 Commits
david/do-n
...
dcreager/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c976dc570 | ||
|
|
b44fb47f25 | ||
|
|
a4531bf865 | ||
|
|
be54b840e9 | ||
|
|
45b5dedee2 | ||
|
|
9ff4772a2c | ||
|
|
c077b109ce | ||
|
|
8a2dd01db4 | ||
|
|
f888e51a34 | ||
|
|
d11e959ad5 | ||
|
|
a56eef444a | ||
|
|
14ff67fd46 | ||
|
|
ada7d4da0d | ||
|
|
4cafb44ba7 | ||
|
|
1445836872 | ||
|
|
da6b68cb58 | ||
|
|
2a478ce1b2 | ||
|
|
8fe2dd5e03 | ||
|
|
0a4dec0323 | ||
|
|
454ad15aee | ||
|
|
fd3fc34a9e | ||
|
|
c550b4d565 | ||
|
|
f8061e8b99 | ||
|
|
27a315b740 | ||
|
|
08221454f6 | ||
|
|
5fec1039ed | ||
|
|
787bcd1c6a | ||
|
|
5853eb28dd | ||
|
|
84d064a14c | ||
|
|
e4e405d2a1 | ||
|
|
1918c61623 | ||
|
|
44ad201262 | ||
|
|
c7372d218d | ||
|
|
de8f4e62e2 | ||
|
|
edfa03a692 | ||
|
|
9965cee998 | ||
|
|
58807b2980 | ||
|
|
9c47b6dbb0 | ||
|
|
d2ebfd6ed7 | ||
|
|
c36f3f5304 | ||
|
|
fcd50a0496 | ||
|
|
3ada36b766 | ||
|
|
bd89838212 | ||
|
|
b32407b6f3 | ||
|
|
b4de245a5a | ||
|
|
914095d08f | ||
|
|
5350288d07 | ||
|
|
649610cc98 | ||
|
|
1a79722ee0 | ||
|
|
b67590bfde | ||
|
|
e6a2de3ac6 | ||
|
|
c7b5067ef8 | ||
|
|
5a115e750d | ||
|
|
a1f361949e | ||
|
|
13ea4e5d0e | ||
|
|
a2a7b1e268 | ||
|
|
1dedcb9e0d | ||
|
|
807a8a7a29 | ||
|
|
78dabc332d | ||
|
|
bfc17fecaa | ||
|
|
942cb9e3ad | ||
|
|
312a487ea7 | ||
|
|
cf8dc60292 | ||
|
|
ea5d5c4e29 | ||
|
|
c99d5522eb | ||
|
|
e57c83e369 | ||
|
|
1d49e71ddd | ||
|
|
f05b2d3673 | ||
|
|
79b921179c | ||
|
|
1f85e0d0a0 | ||
|
|
03adae80dc |
18
.github/workflows/build-binaries.yml
vendored
18
.github/workflows/build-binaries.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build sdist"
|
- name: "Build sdist"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
command: sdist
|
command: sdist
|
||||||
args: --out dist
|
args: --out dist
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels - x86_64"
|
- name: "Build wheels - x86_64"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: x86_64
|
target: x86_64
|
||||||
args: --release --locked --out dist
|
args: --release --locked --out dist
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels - aarch64"
|
- name: "Build wheels - aarch64"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: aarch64
|
target: aarch64
|
||||||
args: --release --locked --out dist
|
args: --release --locked --out dist
|
||||||
@@ -177,7 +177,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels"
|
- name: "Build wheels"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.platform.target }}
|
target: ${{ matrix.platform.target }}
|
||||||
args: --release --locked --out dist
|
args: --release --locked --out dist
|
||||||
@@ -230,7 +230,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels"
|
- name: "Build wheels"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
manylinux: auto
|
manylinux: auto
|
||||||
@@ -304,7 +304,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels"
|
- name: "Build wheels"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.platform.target }}
|
target: ${{ matrix.platform.target }}
|
||||||
manylinux: auto
|
manylinux: auto
|
||||||
@@ -370,14 +370,14 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels"
|
- name: "Build wheels"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
manylinux: musllinux_1_2
|
manylinux: musllinux_1_2
|
||||||
args: --release --locked --out dist
|
args: --release --locked --out dist
|
||||||
- name: "Test wheel"
|
- name: "Test wheel"
|
||||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
uses: addnab/docker-run-action@v3
|
uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3
|
||||||
with:
|
with:
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
options: -v ${{ github.workspace }}:/io -w /io
|
options: -v ${{ github.workspace }}:/io -w /io
|
||||||
@@ -435,7 +435,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels"
|
- name: "Build wheels"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.platform.target }}
|
target: ${{ matrix.platform.target }}
|
||||||
manylinux: musllinux_1_2
|
manylinux: musllinux_1_2
|
||||||
|
|||||||
36
.github/workflows/ci.yaml
vendored
36
.github/workflows/ci.yaml
vendored
@@ -237,13 +237,13 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install mold"
|
- name: "Install mold"
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||||
- name: "Install cargo nextest"
|
- name: "Install cargo nextest"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
- name: "Install cargo insta"
|
- name: "Install cargo insta"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-insta
|
tool: cargo-insta
|
||||||
- name: Red-knot mdtests (GitHub annotations)
|
- name: Red-knot mdtests (GitHub annotations)
|
||||||
@@ -291,13 +291,13 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install mold"
|
- name: "Install mold"
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||||
- name: "Install cargo nextest"
|
- name: "Install cargo nextest"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
- name: "Install cargo insta"
|
- name: "Install cargo insta"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-insta
|
tool: cargo-insta
|
||||||
- name: "Run tests"
|
- name: "Run tests"
|
||||||
@@ -320,7 +320,7 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install cargo nextest"
|
- name: "Install cargo nextest"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
- name: "Run tests"
|
- name: "Run tests"
|
||||||
@@ -376,7 +376,7 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install mold"
|
- name: "Install mold"
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: cargo build --release --locked
|
run: cargo build --release --locked
|
||||||
|
|
||||||
@@ -401,13 +401,13 @@ jobs:
|
|||||||
MSRV: ${{ steps.msrv.outputs.value }}
|
MSRV: ${{ steps.msrv.outputs.value }}
|
||||||
run: rustup default "${MSRV}"
|
run: rustup default "${MSRV}"
|
||||||
- name: "Install mold"
|
- name: "Install mold"
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||||
- name: "Install cargo nextest"
|
- name: "Install cargo nextest"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
- name: "Install cargo insta"
|
- name: "Install cargo insta"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-insta
|
tool: cargo-insta
|
||||||
- name: "Run tests"
|
- name: "Run tests"
|
||||||
@@ -433,7 +433,7 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install cargo-binstall"
|
- name: "Install cargo-binstall"
|
||||||
uses: cargo-bins/cargo-binstall@main
|
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
|
||||||
with:
|
with:
|
||||||
tool: cargo-fuzz@0.11.2
|
tool: cargo-fuzz@0.11.2
|
||||||
- name: "Install cargo-fuzz"
|
- name: "Install cargo-fuzz"
|
||||||
@@ -455,7 +455,7 @@ jobs:
|
|||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||||
name: Download Ruff binary to test
|
name: Download Ruff binary to test
|
||||||
id: download-cached-binary
|
id: download-cached-binary
|
||||||
@@ -641,7 +641,7 @@ jobs:
|
|||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: cargo-bins/cargo-binstall@main
|
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
|
||||||
- run: cargo binstall --no-confirm cargo-shear
|
- run: cargo binstall --no-confirm cargo-shear
|
||||||
- run: cargo shear
|
- run: cargo shear
|
||||||
|
|
||||||
@@ -662,7 +662,7 @@ jobs:
|
|||||||
- name: "Prep README.md"
|
- name: "Prep README.md"
|
||||||
run: python scripts/transform_readme.py --target pypi
|
run: python scripts/transform_readme.py --target pypi
|
||||||
- name: "Build wheels"
|
- name: "Build wheels"
|
||||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||||
with:
|
with:
|
||||||
args: --out dist
|
args: --out dist
|
||||||
- name: "Test wheel"
|
- name: "Test wheel"
|
||||||
@@ -681,7 +681,7 @@ jobs:
|
|||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
- name: "Cache pre-commit"
|
- name: "Cache pre-commit"
|
||||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
with:
|
with:
|
||||||
@@ -720,7 +720,7 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
- name: "Install Insiders dependencies"
|
- name: "Install Insiders dependencies"
|
||||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||||
@@ -857,7 +857,7 @@ jobs:
|
|||||||
run: rustup show
|
run: rustup show
|
||||||
|
|
||||||
- name: "Install codspeed"
|
- name: "Install codspeed"
|
||||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||||
with:
|
with:
|
||||||
tool: cargo-codspeed
|
tool: cargo-codspeed
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,11 +34,11 @@ jobs:
|
|||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install mold"
|
- name: "Install mold"
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||||
- name: Build ruff
|
- name: Build ruff
|
||||||
# A debug build means the script runs slower once it gets started,
|
# A debug build means the script runs slower once it gets started,
|
||||||
|
|||||||
2
.github/workflows/daily_property_tests.yaml
vendored
2
.github/workflows/daily_property_tests.yaml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: "Install Rust toolchain"
|
- name: "Install Rust toolchain"
|
||||||
run: rustup show
|
run: rustup show
|
||||||
- name: "Install mold"
|
- name: "Install mold"
|
||||||
uses: rui314/setup-mold@v1
|
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||||
- name: Build Red Knot
|
- name: Build Red Knot
|
||||||
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
|
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
|
||||||
|
|||||||
7
.github/workflows/mypy_primer.yaml
vendored
7
.github/workflows/mypy_primer.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||||
with:
|
with:
|
||||||
@@ -52,6 +52,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd ruff
|
cd ruff
|
||||||
|
|
||||||
|
PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)"
|
||||||
|
|
||||||
echo "new commit"
|
echo "new commit"
|
||||||
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
||||||
|
|
||||||
@@ -62,13 +64,14 @@ jobs:
|
|||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
echo "Project selector: $PRIMER_SELECTOR"
|
||||||
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
|
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
|
||||||
uvx mypy_primer \
|
uvx mypy_primer \
|
||||||
--repo ruff \
|
--repo ruff \
|
||||||
--type-checker knot \
|
--type-checker knot \
|
||||||
--old base_commit \
|
--old base_commit \
|
||||||
--new "$GITHUB_SHA" \
|
--new "$GITHUB_SHA" \
|
||||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy|werkzeug|bidict|async-utils)$' \
|
--project-selector "/($PRIMER_SELECTOR)\$" \
|
||||||
--output concise \
|
--output concise \
|
||||||
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: "Install uv"
|
- name: "Install uv"
|
||||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||||
with:
|
with:
|
||||||
pattern: wheels-*
|
pattern: wheels-*
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ repos:
|
|||||||
pass_filenames: false # This makes it a lot faster
|
pass_filenames: false # This makes it a lot faster
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.5
|
rev: v0.11.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- id: ruff
|
- id: ruff
|
||||||
@@ -97,7 +97,7 @@ repos:
|
|||||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
rev: v1.5.2
|
rev: v1.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.6
|
||||||
|
|
||||||
|
### Preview features
|
||||||
|
|
||||||
|
- Avoid adding whitespace to the end of a docstring after an escaped quote ([#17216](https://github.com/astral-sh/ruff/pull/17216))
|
||||||
|
- \[`airflow`\] Extract `AIR311` from `AIR301` rules (`AIR301`, `AIR311`) ([#17310](https://github.com/astral-sh/ruff/pull/17310), [#17422](https://github.com/astral-sh/ruff/pull/17422))
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Raise syntax error when `\` is at end of file ([#17409](https://github.com/astral-sh/ruff/pull/17409))
|
||||||
|
|
||||||
## 0.11.5
|
## 0.11.5
|
||||||
|
|
||||||
### Preview features
|
### Preview features
|
||||||
|
|||||||
88
Cargo.lock
generated
88
Cargo.lock
generated
@@ -128,9 +128,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.97"
|
version = "1.0.98"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argfile"
|
name = "argfile"
|
||||||
@@ -216,9 +216,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "1.11.3"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.4.9",
|
"regex-automata 0.4.9",
|
||||||
@@ -334,9 +334,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.35"
|
version = "4.5.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -344,9 +344,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.35"
|
version = "4.5.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -487,7 +487,7 @@ version = "3.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1553,28 +1553,45 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.4"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
|
checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff-static",
|
"jiff-static",
|
||||||
|
"jiff-tzdb-platform",
|
||||||
"log",
|
"log",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"serde",
|
"serde",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-static"
|
name = "jiff-static"
|
||||||
version = "0.2.4"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
|
checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb-platform"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-tzdb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.32"
|
version = "0.1.32"
|
||||||
@@ -1628,9 +1645,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.171"
|
version = "0.2.172"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libcst"
|
name = "libcst"
|
||||||
@@ -1659,9 +1676,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libmimalloc-sys"
|
name = "libmimalloc-sys"
|
||||||
version = "0.1.41"
|
version = "0.1.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1797,9 +1814,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mimalloc"
|
name = "mimalloc"
|
||||||
version = "0.1.45"
|
version = "0.1.46"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libmimalloc-sys",
|
"libmimalloc-sys",
|
||||||
]
|
]
|
||||||
@@ -2310,9 +2327,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -2403,13 +2420,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
"zerocopy",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2476,7 +2492,6 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argfile",
|
"argfile",
|
||||||
"chrono",
|
|
||||||
"clap",
|
"clap",
|
||||||
"colored 3.0.0",
|
"colored 3.0.0",
|
||||||
"countme",
|
"countme",
|
||||||
@@ -2485,6 +2500,7 @@ dependencies = [
|
|||||||
"filetime",
|
"filetime",
|
||||||
"insta",
|
"insta",
|
||||||
"insta-cmd",
|
"insta-cmd",
|
||||||
|
"jiff",
|
||||||
"rayon",
|
"rayon",
|
||||||
"red_knot_project",
|
"red_knot_project",
|
||||||
"red_knot_python_semantic",
|
"red_knot_python_semantic",
|
||||||
@@ -2756,7 +2772,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.11.5"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argfile",
|
"argfile",
|
||||||
@@ -2764,7 +2780,6 @@ dependencies = [
|
|||||||
"bincode",
|
"bincode",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"cachedir",
|
"cachedir",
|
||||||
"chrono",
|
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete_command",
|
"clap_complete_command",
|
||||||
"clearscreen",
|
"clearscreen",
|
||||||
@@ -2777,6 +2792,7 @@ dependencies = [
|
|||||||
"insta-cmd",
|
"insta-cmd",
|
||||||
"is-macro",
|
"is-macro",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"jiff",
|
||||||
"log",
|
"log",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"notify",
|
"notify",
|
||||||
@@ -2991,12 +3007,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff_linter"
|
name = "ruff_linter"
|
||||||
version = "0.11.5"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"chrono",
|
|
||||||
"clap",
|
"clap",
|
||||||
"colored 3.0.0",
|
"colored 3.0.0",
|
||||||
"fern",
|
"fern",
|
||||||
@@ -3007,6 +3022,7 @@ dependencies = [
|
|||||||
"is-macro",
|
"is-macro",
|
||||||
"is-wsl",
|
"is-wsl",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"jiff",
|
||||||
"libcst",
|
"libcst",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3067,7 +3083,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"rand 0.9.0",
|
"rand 0.9.1",
|
||||||
"ruff_diagnostics",
|
"ruff_diagnostics",
|
||||||
"ruff_source_file",
|
"ruff_source_file",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
@@ -3317,7 +3333,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff_wasm"
|
name = "ruff_wasm"
|
||||||
version = "0.11.5"
|
version = "0.11.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
@@ -3658,9 +3674,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellexpand"
|
name = "shellexpand"
|
||||||
version = "3.1.0"
|
version = "3.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
]
|
]
|
||||||
@@ -4311,7 +4327,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"rand 0.9.0",
|
"rand 0.9.1",
|
||||||
"uuid-macro-internal",
|
"uuid-macro-internal",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -4582,7 +4598,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ bitflags = { version = "2.5.0" }
|
|||||||
bstr = { version = "1.9.1" }
|
bstr = { version = "1.9.1" }
|
||||||
cachedir = { version = "0.3.1" }
|
cachedir = { version = "0.3.1" }
|
||||||
camino = { version = "1.1.7" }
|
camino = { version = "1.1.7" }
|
||||||
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
|
|
||||||
clap = { version = "4.5.3", features = ["derive"] }
|
clap = { version = "4.5.3", features = ["derive"] }
|
||||||
clap_complete_command = { version = "0.6.0" }
|
clap_complete_command = { version = "0.6.0" }
|
||||||
clearscreen = { version = "4.0.0" }
|
clearscreen = { version = "4.0.0" }
|
||||||
@@ -95,6 +94,7 @@ insta-cmd = { version = "0.6.0" }
|
|||||||
is-macro = { version = "0.3.5" }
|
is-macro = { version = "0.3.5" }
|
||||||
is-wsl = { version = "0.4.0" }
|
is-wsl = { version = "0.4.0" }
|
||||||
itertools = { version = "0.14.0" }
|
itertools = { version = "0.14.0" }
|
||||||
|
jiff = { version = "0.2.0" }
|
||||||
js-sys = { version = "0.3.69" }
|
js-sys = { version = "0.3.69" }
|
||||||
jod-thread = { version = "0.1.2" }
|
jod-thread = { version = "0.1.2" }
|
||||||
libc = { version = "0.2.153" }
|
libc = { version = "0.2.153" }
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
|||||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||||
|
|
||||||
# For a specific version.
|
# For a specific version.
|
||||||
curl -LsSf https://astral.sh/ruff/0.11.5/install.sh | sh
|
curl -LsSf https://astral.sh/ruff/0.11.6/install.sh | sh
|
||||||
powershell -c "irm https://astral.sh/ruff/0.11.5/install.ps1 | iex"
|
powershell -c "irm https://astral.sh/ruff/0.11.6/install.ps1 | iex"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||||
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
|||||||
```yaml
|
```yaml
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.11.5
|
rev: v0.11.6
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ ruff_python_ast = { workspace = true }
|
|||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
argfile = { workspace = true }
|
argfile = { workspace = true }
|
||||||
chrono = { workspace = true }
|
|
||||||
clap = { workspace = true, features = ["wrap_help"] }
|
clap = { workspace = true, features = ["wrap_help"] }
|
||||||
colored = { workspace = true }
|
colored = { workspace = true }
|
||||||
countme = { workspace = true, features = ["enable"] }
|
countme = { workspace = true, features = ["enable"] }
|
||||||
crossbeam = { workspace = true }
|
crossbeam = { workspace = true }
|
||||||
ctrlc = { version = "3.4.4" }
|
ctrlc = { version = "3.4.4" }
|
||||||
|
jiff = { workspace = true }
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
salsa = { workspace = true }
|
salsa = { workspace = true }
|
||||||
tracing = { workspace = true, features = ["release_max_level_debug"] }
|
tracing = { workspace = true, features = ["release_max_level_debug"] }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ mypy_primer \
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
|
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
|
||||||
diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`.
|
diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt))\$"`.
|
||||||
|
|
||||||
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration
|
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration
|
||||||
option to work correctly.
|
option to work correctly.
|
||||||
|
|||||||
@@ -190,8 +190,8 @@ where
|
|||||||
let ansi = writer.has_ansi_escapes();
|
let ansi = writer.has_ansi_escapes();
|
||||||
|
|
||||||
if self.display_timestamp {
|
if self.display_timestamp {
|
||||||
let timestamp = chrono::Local::now()
|
let timestamp = jiff::Zoned::now()
|
||||||
.format("%Y-%m-%d %H:%M:%S.%f")
|
.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
.to_string();
|
.to_string();
|
||||||
if ansi {
|
if ansi {
|
||||||
write!(writer, "{} ", timestamp.dimmed())?;
|
write!(writer, "{} ", timestamp.dimmed())?;
|
||||||
@@ -199,7 +199,7 @@ where
|
|||||||
write!(
|
write!(
|
||||||
writer,
|
writer,
|
||||||
"{} ",
|
"{} ",
|
||||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f")
|
jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
|||||||
r#"
|
r#"
|
||||||
y = 4 / 0
|
y = 4 / 0
|
||||||
|
|
||||||
for a in range(0, y):
|
for a in range(0, int(y)):
|
||||||
x = a
|
x = a
|
||||||
|
|
||||||
print(x) # possibly-unresolved-reference
|
print(x) # possibly-unresolved-reference
|
||||||
@@ -271,7 +271,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
|||||||
2 | y = 4 / 0
|
2 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
3 |
|
3 |
|
||||||
4 | for a in range(0, y):
|
4 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
warning: lint:possibly-unresolved-reference
|
warning: lint:possibly-unresolved-reference
|
||||||
@@ -307,7 +307,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
|||||||
2 | y = 4 / 0
|
2 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
3 |
|
3 |
|
||||||
4 | for a in range(0, y):
|
4 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
Found 1 diagnostic
|
Found 1 diagnostic
|
||||||
@@ -328,7 +328,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
y = 4 / 0
|
y = 4 / 0
|
||||||
|
|
||||||
for a in range(0, y):
|
for a in range(0, int(y)):
|
||||||
x = a
|
x = a
|
||||||
|
|
||||||
print(x) # possibly-unresolved-reference
|
print(x) # possibly-unresolved-reference
|
||||||
@@ -358,7 +358,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
|||||||
4 | y = 4 / 0
|
4 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
5 |
|
5 |
|
||||||
6 | for a in range(0, y):
|
6 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
warning: lint:possibly-unresolved-reference
|
warning: lint:possibly-unresolved-reference
|
||||||
@@ -405,7 +405,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
|||||||
4 | y = 4 / 0
|
4 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
5 |
|
5 |
|
||||||
6 | for a in range(0, y):
|
6 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
Found 2 diagnostics
|
Found 2 diagnostics
|
||||||
@@ -426,7 +426,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
|||||||
r#"
|
r#"
|
||||||
y = 4 / 0
|
y = 4 / 0
|
||||||
|
|
||||||
for a in range(0, y):
|
for a in range(0, int(y)):
|
||||||
x = a
|
x = a
|
||||||
|
|
||||||
print(x) # possibly-unresolved-reference
|
print(x) # possibly-unresolved-reference
|
||||||
@@ -445,7 +445,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
|||||||
2 | y = 4 / 0
|
2 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
3 |
|
3 |
|
||||||
4 | for a in range(0, y):
|
4 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
warning: lint:possibly-unresolved-reference
|
warning: lint:possibly-unresolved-reference
|
||||||
@@ -482,7 +482,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
|||||||
2 | y = 4 / 0
|
2 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
3 |
|
3 |
|
||||||
4 | for a in range(0, y):
|
4 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
Found 1 diagnostic
|
Found 1 diagnostic
|
||||||
@@ -814,7 +814,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
|||||||
r#"
|
r#"
|
||||||
y = 4 / 0
|
y = 4 / 0
|
||||||
|
|
||||||
for a in range(0, y):
|
for a in range(0, int(y)):
|
||||||
x = a
|
x = a
|
||||||
|
|
||||||
print(x)
|
print(x)
|
||||||
@@ -841,7 +841,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
|||||||
2 | y = 4 / 0
|
2 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
3 |
|
3 |
|
||||||
4 | for a in range(0, y):
|
4 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
warning: lint:possibly-unresolved-reference
|
warning: lint:possibly-unresolved-reference
|
||||||
@@ -883,7 +883,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
|||||||
2 | y = 4 / 0
|
2 | y = 4 / 0
|
||||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||||
3 |
|
3 |
|
||||||
4 | for a in range(0, y):
|
4 | for a in range(0, int(y)):
|
||||||
|
|
|
|
||||||
|
|
||||||
error: lint:possibly-unresolved-reference
|
error: lint:possibly-unresolved-reference
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
use super::Db;
|
use super::Db;
|
||||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||||
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb};
|
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
|
||||||
use ruff_db::files::{File, Files};
|
use ruff_db::files::{File, Files};
|
||||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||||
use ruff_db::vendored::VendoredFileSystem;
|
use ruff_db::vendored::VendoredFileSystem;
|
||||||
@@ -83,6 +83,10 @@ pub(crate) mod tests {
|
|||||||
fn files(&self) -> &Files {
|
fn files(&self) -> &Files {
|
||||||
&self.files
|
&self.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||||
|
Program::get(self).python_version(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Upcast<dyn SourceDb> for TestDb {
|
impl Upcast<dyn SourceDb> for TestDb {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Regression test for https://github.com/astral-sh/ruff/issues/17215
|
||||||
|
# panicked in commit 1a6a10b30
|
||||||
|
# error message:
|
||||||
|
# dependency graph cycle querying all_narrowing_constraints_for_expression(Id(8591))
|
||||||
|
|
||||||
|
def f(a: A, b: B, c: C):
|
||||||
|
unknown_a: UA = make_unknown()
|
||||||
|
unknown_b: UB = make_unknown()
|
||||||
|
unknown_c: UC = make_unknown()
|
||||||
|
unknown_d: UD = make_unknown()
|
||||||
|
|
||||||
|
if unknown_a and unknown_b:
|
||||||
|
if unknown_c:
|
||||||
|
if unknown_d:
|
||||||
|
return a, b, c
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Regression test for https://github.com/astral-sh/ruff/issues/17215
|
||||||
|
# panicked in commit 1a6a10b30
|
||||||
|
# error message:
|
||||||
|
# dependency graph cycle querying all_negative_narrowing_constraints_for_expression(Id(859f))
|
||||||
|
|
||||||
|
def f(f1: bool, f2: bool, f3: bool, f4: bool):
|
||||||
|
o1: UnknownClass = make_o()
|
||||||
|
o2: UnknownClass = make_o()
|
||||||
|
o3: UnknownClass = make_o()
|
||||||
|
o4: UnknownClass = make_o()
|
||||||
|
|
||||||
|
if f1 and f2 and f3 and f4:
|
||||||
|
if o1 == o2:
|
||||||
|
return None
|
||||||
|
if o2 == o3:
|
||||||
|
return None
|
||||||
|
if o3 == o4:
|
||||||
|
return None
|
||||||
|
if o4 == o1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return o1, o2, o3, o4
|
||||||
@@ -149,6 +149,10 @@ impl SourceDb for ProjectDatabase {
|
|||||||
fn files(&self) -> &Files {
|
fn files(&self) -> &Files {
|
||||||
&self.files
|
&self.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||||
|
Program::get(self).python_version(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[salsa::db]
|
#[salsa::db]
|
||||||
@@ -207,7 +211,7 @@ pub(crate) mod tests {
|
|||||||
use salsa::Event;
|
use salsa::Event;
|
||||||
|
|
||||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||||
use red_knot_python_semantic::Db as SemanticDb;
|
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||||
use ruff_db::files::Files;
|
use ruff_db::files::Files;
|
||||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||||
use ruff_db::vendored::VendoredFileSystem;
|
use ruff_db::vendored::VendoredFileSystem;
|
||||||
@@ -281,6 +285,10 @@ pub(crate) mod tests {
|
|||||||
fn files(&self) -> &Files {
|
fn files(&self) -> &Files {
|
||||||
&self.files
|
&self.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||||
|
Program::get(self).python_version(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Upcast<dyn SemanticDb> for TestDb {
|
impl Upcast<dyn SemanticDb> for TestDb {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSele
|
|||||||
use red_knot_python_semantic::register_lints;
|
use red_knot_python_semantic::register_lints;
|
||||||
use red_knot_python_semantic::types::check_types;
|
use red_knot_python_semantic::types::check_types;
|
||||||
use ruff_db::diagnostic::{
|
use ruff_db::diagnostic::{
|
||||||
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span,
|
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
|
||||||
|
DiagnosticId, Severity, Span,
|
||||||
};
|
};
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_db::parsed::parsed_module;
|
use ruff_db::parsed::parsed_module;
|
||||||
@@ -424,6 +425,13 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
|||||||
.map(|error| create_parse_diagnostic(file, error)),
|
.map(|error| create_parse_diagnostic(file, error)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
diagnostics.extend(
|
||||||
|
parsed
|
||||||
|
.unsupported_syntax_errors()
|
||||||
|
.iter()
|
||||||
|
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
|
||||||
|
);
|
||||||
|
|
||||||
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
|
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
|
||||||
|
|
||||||
diagnostics.sort_unstable_by_key(|diagnostic| {
|
diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||||
@@ -520,11 +528,13 @@ mod tests {
|
|||||||
use crate::db::tests::TestDb;
|
use crate::db::tests::TestDb;
|
||||||
use crate::{check_file_impl, ProjectMetadata};
|
use crate::{check_file_impl, ProjectMetadata};
|
||||||
use red_knot_python_semantic::types::check_types;
|
use red_knot_python_semantic::types::check_types;
|
||||||
|
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
|
||||||
use ruff_db::files::system_path_to_file;
|
use ruff_db::files::system_path_to_file;
|
||||||
use ruff_db::source::source_text;
|
use ruff_db::source::source_text;
|
||||||
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
||||||
use ruff_db::testing::assert_function_query_was_not_run;
|
use ruff_db::testing::assert_function_query_was_not_run;
|
||||||
use ruff_python_ast::name::Name;
|
use ruff_python_ast::name::Name;
|
||||||
|
use ruff_python_ast::PythonVersion;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||||
@@ -532,6 +542,16 @@ mod tests {
|
|||||||
let mut db = TestDb::new(project);
|
let mut db = TestDb::new(project);
|
||||||
let path = SystemPath::new("test.py");
|
let path = SystemPath::new("test.py");
|
||||||
|
|
||||||
|
Program::from_settings(
|
||||||
|
&db,
|
||||||
|
ProgramSettings {
|
||||||
|
python_version: PythonVersion::default(),
|
||||||
|
python_platform: PythonPlatform::default(),
|
||||||
|
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Failed to configure program settings");
|
||||||
|
|
||||||
db.write_file(path, "x = 10")?;
|
db.write_file(path, "x = 10")?;
|
||||||
let file = system_path_to_file(&db, path).unwrap();
|
let file = system_path_to_file(&db, path).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use ruff_db::parsed::parsed_module;
|
|||||||
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
|
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
|
||||||
use ruff_python_ast::visitor::source_order;
|
use ruff_python_ast::visitor::source_order;
|
||||||
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
||||||
use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt};
|
use ruff_python_ast::{
|
||||||
|
self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt,
|
||||||
|
};
|
||||||
|
|
||||||
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
|
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
|
||||||
let project = ProjectMetadata::discover(project_root, &system)?;
|
let project = ProjectMetadata::discover(project_root, &system)?;
|
||||||
@@ -258,6 +260,14 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
|||||||
source_order::walk_expr(self, expr);
|
source_order::walk_expr(self, expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
|
||||||
|
self.visit_expr(&comprehension.iter);
|
||||||
|
self.visit_target(&comprehension.target);
|
||||||
|
for if_expr in &comprehension.ifs {
|
||||||
|
self.visit_expr(if_expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn visit_parameter(&mut self, parameter: &Parameter) {
|
fn visit_parameter(&mut self, parameter: &Parameter) {
|
||||||
let _ty = parameter.inferred_type(&self.model);
|
let _ty = parameter.inferred_type(&self.model);
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,11 @@ def _(c: Callable[[Concatenate[int, str, ...], int], int]):
|
|||||||
|
|
||||||
## Using `typing.ParamSpec`
|
## Using `typing.ParamSpec`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
Using a `ParamSpec` in a `Callable` annotation:
|
Using a `ParamSpec` in a `Callable` annotation:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ reveal_type(get_foo()) # revealed: Foo
|
|||||||
|
|
||||||
## Deferred self-reference annotations in a class definition
|
## Deferred self-reference annotations in a class definition
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -94,6 +99,11 @@ class Foo:
|
|||||||
|
|
||||||
## Non-deferred self-reference annotations in a class definition
|
## Non-deferred self-reference annotations in a class definition
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Foo:
|
class Foo:
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
@@ -146,3 +156,24 @@ def _():
|
|||||||
def f(self) -> C:
|
def f(self) -> C:
|
||||||
return self
|
return self
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Base class references
|
||||||
|
|
||||||
|
### Not deferred by __future__.annotations
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A(B): # error: [unresolved-reference]
|
||||||
|
pass
|
||||||
|
|
||||||
|
class B:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deferred in stub files
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
class A(B): ...
|
||||||
|
class B: ...
|
||||||
|
```
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ def _(
|
|||||||
reveal_type(k) # revealed: Unknown
|
reveal_type(k) # revealed: Unknown
|
||||||
reveal_type(p) # revealed: Unknown
|
reveal_type(p) # revealed: Unknown
|
||||||
reveal_type(q) # revealed: int | Unknown
|
reveal_type(q) # revealed: int | Unknown
|
||||||
reveal_type(r) # revealed: @Todo(generics)
|
reveal_type(r) # revealed: @Todo(unknown type subscript)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Invalid Collection based AST nodes
|
## Invalid Collection based AST nodes
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def x(
|
|||||||
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
|
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
|
||||||
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
|
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
|
||||||
):
|
):
|
||||||
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
|
reveal_type(a1) # revealed: Literal[1, 2, 3, 5, "foo"] | None
|
||||||
reveal_type(a2) # revealed: Literal["w", "r"]
|
reveal_type(a2) # revealed: Literal["w", "r"]
|
||||||
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
|
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
|
||||||
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
|
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
|
||||||
@@ -108,7 +108,7 @@ def union_example(
|
|||||||
None,
|
None,
|
||||||
],
|
],
|
||||||
):
|
):
|
||||||
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
|
reveal_type(x) # revealed: Unknown | Literal[-1, 0, 1, "A", "B", "foo", "bar", b"A", b"\x00", b"\x07", True] | None
|
||||||
```
|
```
|
||||||
|
|
||||||
## Detecting Literal outside typing and typing_extensions
|
## Detecting Literal outside typing and typing_extensions
|
||||||
@@ -137,7 +137,7 @@ from other import Literal
|
|||||||
a1: Literal[26]
|
a1: Literal[26]
|
||||||
|
|
||||||
def f():
|
def f():
|
||||||
reveal_type(a1) # revealed: @Todo(generics)
|
reveal_type(a1) # revealed: @Todo(unknown type subscript)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Detecting typing_extensions.Literal
|
## Detecting typing_extensions.Literal
|
||||||
|
|||||||
@@ -72,13 +72,11 @@ reveal_type(baz) # revealed: Literal["bazfoo"]
|
|||||||
qux = (foo, bar)
|
qux = (foo, bar)
|
||||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||||
|
|
||||||
# TODO: Infer "LiteralString"
|
reveal_type(foo.join(qux)) # revealed: LiteralString
|
||||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function)
|
|
||||||
|
|
||||||
template: LiteralString = "{}, {}"
|
template: LiteralString = "{}, {}"
|
||||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||||
# TODO: Infer `LiteralString`
|
reveal_type(template.format(foo, bar)) # revealed: LiteralString
|
||||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Assignability
|
### Assignability
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Starred expression annotations
|
# Starred expression annotations
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.11"
|
||||||
|
```
|
||||||
|
|
||||||
Type annotations for `*args` can be starred expressions themselves:
|
Type annotations for `*args` can be starred expressions themselves:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
@@ -67,21 +67,24 @@ import typing
|
|||||||
|
|
||||||
####################
|
####################
|
||||||
### Built-ins
|
### Built-ins
|
||||||
|
####################
|
||||||
|
|
||||||
class ListSubclass(typing.List): ...
|
class ListSubclass(typing.List): ...
|
||||||
|
|
||||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
# TODO: generic protocols
|
||||||
|
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||||
reveal_type(ListSubclass.__mro__)
|
reveal_type(ListSubclass.__mro__)
|
||||||
|
|
||||||
class DictSubclass(typing.Dict): ...
|
class DictSubclass(typing.Dict): ...
|
||||||
|
|
||||||
# TODO: should have `Generic`, should not have `Unknown`
|
# TODO: generic protocols
|
||||||
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
|
# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||||
reveal_type(DictSubclass.__mro__)
|
reveal_type(DictSubclass.__mro__)
|
||||||
|
|
||||||
class SetSubclass(typing.Set): ...
|
class SetSubclass(typing.Set): ...
|
||||||
|
|
||||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
# TODO: generic protocols
|
||||||
|
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||||
reveal_type(SetSubclass.__mro__)
|
reveal_type(SetSubclass.__mro__)
|
||||||
|
|
||||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||||
@@ -92,11 +95,12 @@ reveal_type(FrozenSetSubclass.__mro__)
|
|||||||
|
|
||||||
####################
|
####################
|
||||||
### `collections`
|
### `collections`
|
||||||
|
####################
|
||||||
|
|
||||||
class ChainMapSubclass(typing.ChainMap): ...
|
class ChainMapSubclass(typing.ChainMap): ...
|
||||||
|
|
||||||
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
# TODO: generic protocols
|
||||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
|
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||||
reveal_type(ChainMapSubclass.__mro__)
|
reveal_type(ChainMapSubclass.__mro__)
|
||||||
|
|
||||||
class CounterSubclass(typing.Counter): ...
|
class CounterSubclass(typing.Counter): ...
|
||||||
@@ -113,7 +117,8 @@ reveal_type(DefaultDictSubclass.__mro__)
|
|||||||
|
|
||||||
class DequeSubclass(typing.Deque): ...
|
class DequeSubclass(typing.Deque): ...
|
||||||
|
|
||||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
# TODO: generic protocols
|
||||||
|
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||||
reveal_type(DequeSubclass.__mro__)
|
reveal_type(DequeSubclass.__mro__)
|
||||||
|
|
||||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def f1(
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
||||||
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
|
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Class variables
|
## Class variables
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Foo:
|
|||||||
One thing that is supported is error messages for using special forms in type expressions.
|
One thing that is supported is error messages for using special forms in type expressions.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec
|
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic
|
||||||
|
|
||||||
def _(
|
def _(
|
||||||
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
|
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
|
||||||
@@ -49,6 +49,7 @@ def _(
|
|||||||
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
|
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
|
||||||
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
|
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
|
||||||
e: ParamSpec,
|
e: ParamSpec,
|
||||||
|
f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions"
|
||||||
) -> None:
|
) -> None:
|
||||||
reveal_type(a) # revealed: Unknown
|
reveal_type(a) # revealed: Unknown
|
||||||
reveal_type(b) # revealed: Unknown
|
reveal_type(b) # revealed: Unknown
|
||||||
@@ -65,7 +66,7 @@ You can't inherit from most of these. `typing.Callable` is an exception.
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
|
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic
|
||||||
|
|
||||||
class A(Self): ... # error: [invalid-base]
|
class A(Self): ... # error: [invalid-base]
|
||||||
class B(Unpack): ... # error: [invalid-base]
|
class B(Unpack): ... # error: [invalid-base]
|
||||||
@@ -73,12 +74,18 @@ class C(TypeGuard): ... # error: [invalid-base]
|
|||||||
class D(TypeIs): ... # error: [invalid-base]
|
class D(TypeIs): ... # error: [invalid-base]
|
||||||
class E(Concatenate): ... # error: [invalid-base]
|
class E(Concatenate): ... # error: [invalid-base]
|
||||||
class F(Callable): ...
|
class F(Callable): ...
|
||||||
|
class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`"
|
||||||
|
|
||||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Subscriptability
|
## Subscriptability
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
Some of these are not subscriptable:
|
Some of these are not subscriptable:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
|
|||||||
|
|
||||||
## Tuple annotations are understood
|
## Tuple annotations are understood
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
`module.py`:
|
`module.py`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -56,7 +61,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
|||||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||||
reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)]
|
reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)]
|
||||||
|
|
||||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||||
reveal_type(j) # revealed: tuple[str | int]
|
reveal_type(j) # revealed: tuple[str | int]
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ class C:
|
|||||||
|
|
||||||
c_instance = C()
|
c_instance = C()
|
||||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||||
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
|
reveal_type(c_instance.b) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Attributes defined in for-loop (unpacking)
|
#### Attributes defined in for-loop (unpacking)
|
||||||
@@ -397,15 +397,27 @@ class IntIterable:
|
|||||||
def __iter__(self) -> IntIterator:
|
def __iter__(self) -> IntIterator:
|
||||||
return IntIterator()
|
return IntIterator()
|
||||||
|
|
||||||
|
class TupleIterator:
|
||||||
|
def __next__(self) -> tuple[int, str]:
|
||||||
|
return (1, "a")
|
||||||
|
|
||||||
|
class TupleIterable:
|
||||||
|
def __iter__(self) -> TupleIterator:
|
||||||
|
return TupleIterator()
|
||||||
|
|
||||||
class C:
|
class C:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
[... for self.a in IntIterable()]
|
[... for self.a in IntIterable()]
|
||||||
|
[... for (self.b, self.c) in TupleIterable()]
|
||||||
|
[... for self.d in IntIterable() for self.e in IntIterable()]
|
||||||
|
|
||||||
c_instance = C()
|
c_instance = C()
|
||||||
|
|
||||||
# TODO: Should be `Unknown | int`
|
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||||
# error: [unresolved-attribute]
|
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||||
reveal_type(c_instance.a) # revealed: Unknown
|
reveal_type(c_instance.c) # revealed: Unknown | str
|
||||||
|
reveal_type(c_instance.d) # revealed: Unknown | int
|
||||||
|
reveal_type(c_instance.e) # revealed: Unknown | int
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Conditionally declared / bound attributes
|
#### Conditionally declared / bound attributes
|
||||||
@@ -1665,7 +1677,7 @@ functions are instances of that class:
|
|||||||
def f(): ...
|
def f(): ...
|
||||||
|
|
||||||
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
|
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
|
||||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
|
reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
|
||||||
```
|
```
|
||||||
|
|
||||||
Some attributes are special-cased, however:
|
Some attributes are special-cased, however:
|
||||||
@@ -1698,9 +1710,9 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
|
|||||||
bools are instances of that class:
|
bools are instances of that class:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# revealed: bound method Literal[True].__and__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function)
|
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
|
||||||
reveal_type(True.__and__)
|
reveal_type(True.__and__)
|
||||||
# revealed: bound method Literal[False].__or__(**kwargs: @Todo(todo signature **kwargs)) -> @Todo(return type of overloaded function)
|
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
|
||||||
reveal_type(False.__or__)
|
reveal_type(False.__or__)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1716,7 +1728,8 @@ reveal_type(False.real) # revealed: Literal[0]
|
|||||||
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
|
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
reveal_type(b"foo".join) # revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(generics), /) -> bytes
|
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes
|
||||||
|
reveal_type(b"foo".join)
|
||||||
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
|
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
|
||||||
reveal_type(b"foo".endswith)
|
reveal_type(b"foo".endswith)
|
||||||
```
|
```
|
||||||
@@ -1819,6 +1832,89 @@ def f(never: Never):
|
|||||||
never.another_attribute = never
|
never.another_attribute = never
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Cyclic implicit attributes
|
||||||
|
|
||||||
|
Inferring types for undeclared implicit attributes can be cyclic:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
|
||||||
|
def copy(self, other: "C"):
|
||||||
|
self.x = other.x
|
||||||
|
|
||||||
|
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class D:
|
||||||
|
def copy(self, other: "D"):
|
||||||
|
self.x = other.x
|
||||||
|
|
||||||
|
reveal_type(D().x) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to
|
||||||
|
that name, so these cases don't trigger any cycle:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class E:
|
||||||
|
def __init__(self):
|
||||||
|
self.x: int = 1
|
||||||
|
|
||||||
|
def copy(self, other: "E"):
|
||||||
|
self.x = other.x
|
||||||
|
|
||||||
|
reveal_type(E().x) # revealed: int
|
||||||
|
|
||||||
|
class F:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
|
||||||
|
def copy(self, other: "F"):
|
||||||
|
self.x: int = other.x
|
||||||
|
|
||||||
|
reveal_type(F().x) # revealed: int
|
||||||
|
|
||||||
|
class G:
|
||||||
|
def copy(self, other: "G"):
|
||||||
|
self.x: int = other.x
|
||||||
|
|
||||||
|
reveal_type(G().x) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
We can even handle cycles involving multiple classes:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
|
||||||
|
def copy(self, other: "B"):
|
||||||
|
self.x = other.x
|
||||||
|
|
||||||
|
class B:
|
||||||
|
def copy(self, other: "A"):
|
||||||
|
self.x = other.x
|
||||||
|
|
||||||
|
reveal_type(B().x) # revealed: Unknown | Literal[1]
|
||||||
|
reveal_type(A().x) # revealed: Unknown | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
This case additionally tests our union/intersection simplification logic:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class H:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
|
||||||
|
def copy(self, other: "H"):
|
||||||
|
self.x = other.x or self.x
|
||||||
|
```
|
||||||
|
|
||||||
### Builtin types attributes
|
### Builtin types attributes
|
||||||
|
|
||||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||||
@@ -1870,20 +1966,6 @@ reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
|
|||||||
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||||
```
|
```
|
||||||
|
|
||||||
## `super()`
|
|
||||||
|
|
||||||
`super()` is not supported yet, but we do not emit false positives on `super()` calls.
|
|
||||||
|
|
||||||
```py
|
|
||||||
class Foo:
|
|
||||||
def bar(self) -> int:
|
|
||||||
return 42
|
|
||||||
|
|
||||||
class Bar(Foo):
|
|
||||||
def bar(self) -> int:
|
|
||||||
return super().bar()
|
|
||||||
```
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||||
|
|||||||
@@ -310,9 +310,7 @@ reveal_type(A() + 1) # revealed: A
|
|||||||
reveal_type(1 + A()) # revealed: A
|
reveal_type(1 + A()) # revealed: A
|
||||||
|
|
||||||
reveal_type(A() + "foo") # revealed: A
|
reveal_type(A() + "foo") # revealed: A
|
||||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
reveal_type("foo" + A()) # revealed: A
|
||||||
# TODO overloads
|
|
||||||
reveal_type("foo" + A()) # revealed: @Todo(return type of overloaded function)
|
|
||||||
|
|
||||||
reveal_type(A() + b"foo") # revealed: A
|
reveal_type(A() + b"foo") # revealed: A
|
||||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||||
@@ -320,16 +318,14 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
|||||||
|
|
||||||
reveal_type(A() + ()) # revealed: A
|
reveal_type(A() + ()) # revealed: A
|
||||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||||
reveal_type(() + A()) # revealed: @Todo(return type of overloaded function)
|
reveal_type(() + A()) # revealed: @Todo(full tuple[...] support)
|
||||||
|
|
||||||
literal_string_instance = "foo" * 1_000_000_000
|
literal_string_instance = "foo" * 1_000_000_000
|
||||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||||
reveal_type(literal_string_instance) # revealed: LiteralString
|
reveal_type(literal_string_instance) # revealed: LiteralString
|
||||||
|
|
||||||
reveal_type(A() + literal_string_instance) # revealed: A
|
reveal_type(A() + literal_string_instance) # revealed: A
|
||||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
reveal_type(literal_string_instance + A()) # revealed: A
|
||||||
# TODO overloads
|
|
||||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of overloaded function)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Operations involving instances of classes inheriting from `Any`
|
## Operations involving instances of classes inheriting from `Any`
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
|||||||
reveal_type(2**largest_u32) # revealed: int
|
reveal_type(2**largest_u32) # revealed: int
|
||||||
|
|
||||||
def variable(x: int):
|
def variable(x: int):
|
||||||
reveal_type(x**2) # revealed: @Todo(return type of overloaded function)
|
reveal_type(x**2) # revealed: int
|
||||||
reveal_type(2**x) # revealed: @Todo(return type of overloaded function)
|
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||||
reveal_type(x**x) # revealed: @Todo(return type of overloaded function)
|
reveal_type(2**x) # revealed: int
|
||||||
|
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||||
|
reveal_type(x**x) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ if True and (x := 1):
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
def _(flag: bool):
|
def _(flag: bool):
|
||||||
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
flag or (x := 1) or reveal_type(x) # revealed: Never
|
||||||
|
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
flag or reveal_type(y) or (y := 1) # revealed: Unknown
|
flag or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
|||||||
|
|
||||||
## Generic
|
## Generic
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def get_int[T]() -> int:
|
def get_int[T]() -> int:
|
||||||
return 42
|
return 42
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd
|
|||||||
methods, even though it is not available on `types.MethodType`:
|
methods, even though it is not available on `types.MethodType`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
|
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
|
||||||
```
|
```
|
||||||
|
|
||||||
## Basic method calls on class objects and instances
|
## Basic method calls on class objects and instances
|
||||||
@@ -399,6 +399,11 @@ reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound metho
|
|||||||
|
|
||||||
### Classmethods mixed with other decorators
|
### Classmethods mixed with other decorators
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
|
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
|
||||||
class method:
|
class method:
|
||||||
|
|
||||||
@@ -410,29 +415,19 @@ def does_nothing[T](f: T) -> T:
|
|||||||
|
|
||||||
class C:
|
class C:
|
||||||
@classmethod
|
@classmethod
|
||||||
# TODO: no error should be emitted here (needs support for generics)
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
@does_nothing
|
@does_nothing
|
||||||
def f1(cls: type[C], x: int) -> str:
|
def f1(cls: type[C], x: int) -> str:
|
||||||
return "a"
|
return "a"
|
||||||
# TODO: no error should be emitted here (needs support for generics)
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
@does_nothing
|
@does_nothing
|
||||||
@classmethod
|
@classmethod
|
||||||
def f2(cls: type[C], x: int) -> str:
|
def f2(cls: type[C], x: int) -> str:
|
||||||
return "a"
|
return "a"
|
||||||
|
|
||||||
# TODO: All of these should be `str` (and not emit an error), once we support generics
|
reveal_type(C.f1(1)) # revealed: str
|
||||||
|
reveal_type(C().f1(1)) # revealed: str
|
||||||
# error: [call-non-callable]
|
reveal_type(C.f2(1)) # revealed: str
|
||||||
reveal_type(C.f1(1)) # revealed: Unknown
|
reveal_type(C().f2(1)) # revealed: str
|
||||||
# error: [call-non-callable]
|
|
||||||
reveal_type(C().f1(1)) # revealed: Unknown
|
|
||||||
|
|
||||||
# error: [call-non-callable]
|
|
||||||
reveal_type(C.f2(1)) # revealed: Unknown
|
|
||||||
# error: [call-non-callable]
|
|
||||||
reveal_type(C().f2(1)) # revealed: Unknown
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||||
|
|||||||
@@ -175,3 +175,41 @@ def _(flag: bool):
|
|||||||
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
|
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
|
||||||
reveal_type(f(int)) # revealed: str | Literal[True]
|
reveal_type(f(int)) # revealed: str | Literal[True]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Size limit on unions of literals
|
||||||
|
|
||||||
|
Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`,
|
||||||
|
`bytes`, `str`).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def _(literals_2: Literal[0, 1], b: bool, flag: bool):
|
||||||
|
literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3]
|
||||||
|
literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15]
|
||||||
|
literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63]
|
||||||
|
literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127]
|
||||||
|
|
||||||
|
# Going beyond the MAX_UNION_LITERALS limit (currently 200):
|
||||||
|
literals_256 = 16 * literals_16 + literals_16
|
||||||
|
reveal_type(literals_256) # revealed: int
|
||||||
|
|
||||||
|
# Going beyond the limit when another type is already part of the union
|
||||||
|
bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127]
|
||||||
|
literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255]
|
||||||
|
|
||||||
|
# Now union the two:
|
||||||
|
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simplifying gradually-equivalent types
|
||||||
|
|
||||||
|
If two types are gradually equivalent, we can keep just one of them in a union:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Any, Union
|
||||||
|
from knot_extensions import Intersection, Not
|
||||||
|
|
||||||
|
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
|
||||||
|
reveal_type(x) # revealed: Any & ~int
|
||||||
|
```
|
||||||
|
|||||||
410
crates/red_knot_python_semantic/resources/mdtest/class/super.md
Normal file
410
crates/red_knot_python_semantic/resources/mdtest/class/super.md
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
# Super
|
||||||
|
|
||||||
|
Python defines the terms *bound super object* and *unbound super object*.
|
||||||
|
|
||||||
|
An **unbound super object** is created when `super` is called with only one argument. (e.g.
|
||||||
|
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
|
||||||
|
rarely used in practice.
|
||||||
|
|
||||||
|
A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the
|
||||||
|
implicit form `super()`, where both the pivot class and the owner are inferred. This is the most
|
||||||
|
common usage.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Explicit Super Object
|
||||||
|
|
||||||
|
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
|
||||||
|
specified pivot class.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def a(self): ...
|
||||||
|
aa: int = 1
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def b(self): ...
|
||||||
|
bb: int = 2
|
||||||
|
|
||||||
|
class C(B):
|
||||||
|
def c(self): ...
|
||||||
|
cc: int = 3
|
||||||
|
|
||||||
|
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||||
|
|
||||||
|
super(C, C()).a
|
||||||
|
super(C, C()).b
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
|
||||||
|
super(C, C()).c
|
||||||
|
|
||||||
|
super(B, C()).a
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
|
||||||
|
super(B, C()).b
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
|
||||||
|
super(B, C()).c
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
|
||||||
|
super(A, C()).a
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
|
||||||
|
super(A, C()).b
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
|
||||||
|
super(A, C()).c
|
||||||
|
|
||||||
|
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
|
||||||
|
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
|
||||||
|
reveal_type(super(C, C()).aa) # revealed: int
|
||||||
|
reveal_type(super(C, C()).bb) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implicit Super Object
|
||||||
|
|
||||||
|
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
|
||||||
|
to the class that contains the function where `super()` is used. The first argument refers to the
|
||||||
|
current method’s first parameter (typically `self` or `cls`).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __init__(self, a: int): ...
|
||||||
|
@classmethod
|
||||||
|
def f(cls): ...
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def __init__(self, a: int):
|
||||||
|
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
super().__init__(a)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def f(cls):
|
||||||
|
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
super().f()
|
||||||
|
|
||||||
|
super(B, B(42)).__init__(42)
|
||||||
|
super(B, B).f()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unbound Super Object
|
||||||
|
|
||||||
|
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
|
||||||
|
a plain `super` instance and does not support name lookup via the MRO.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
a: int = 42
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
reveal_type(super(B)) # revealed: super
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
|
||||||
|
super(B).a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attribute Assignment
|
||||||
|
|
||||||
|
`super()` objects do not allow attribute assignment — even if the attribute is resolved
|
||||||
|
successfully.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
a: int = 3
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
reveal_type(super(B, B()).a) # revealed: int
|
||||||
|
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
|
||||||
|
super(B, B()).a = 3
|
||||||
|
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
|
||||||
|
super(B).a = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Types
|
||||||
|
|
||||||
|
If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a
|
||||||
|
member, it should effectively behave like a dynamic type.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
a: int = 1
|
||||||
|
|
||||||
|
def f(x):
|
||||||
|
reveal_type(x) # revealed: Unknown
|
||||||
|
|
||||||
|
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
|
||||||
|
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
|
||||||
|
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
|
||||||
|
|
||||||
|
reveal_type(super(x, x).a) # revealed: Unknown
|
||||||
|
reveal_type(super(A, x).a) # revealed: Unknown
|
||||||
|
reveal_type(super(x, A()).a) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implicit `super()` in Complex Structure
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def test(self):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
|
||||||
|
|
||||||
|
class B:
|
||||||
|
def test(self):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
|
||||||
|
class C(A.B):
|
||||||
|
def test(self):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
|
||||||
|
|
||||||
|
def inner(t: C):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], C>
|
||||||
|
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-ins and Literals
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
|
||||||
|
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
|
||||||
|
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
|
||||||
|
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
|
||||||
|
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Descriptor Behavior with Super
|
||||||
|
|
||||||
|
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
|
||||||
|
differ depending on whether the second argument to `super` is a class or an instance.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def a1(self): ...
|
||||||
|
@classmethod
|
||||||
|
def a2(cls): ...
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
# A.__dict__["a1"].__get__(B(), B)
|
||||||
|
reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown
|
||||||
|
# A.__dict__["a2"].__get__(B(), B)
|
||||||
|
reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
|
||||||
|
|
||||||
|
# A.__dict__["a1"].__get__(None, B)
|
||||||
|
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
|
||||||
|
# A.__dict__["a2"].__get__(None, B)
|
||||||
|
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Union of Supers
|
||||||
|
|
||||||
|
When the owner is a union type, `super()` is built separately for each branch, and the resulting
|
||||||
|
super objects are combined into a union.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
class B:
|
||||||
|
b: int = 42
|
||||||
|
|
||||||
|
class C(A, B): ...
|
||||||
|
class D(B, A): ...
|
||||||
|
|
||||||
|
def f(x: C | D):
|
||||||
|
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||||
|
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
|
||||||
|
|
||||||
|
s = super(A, x)
|
||||||
|
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
|
||||||
|
s.b
|
||||||
|
|
||||||
|
def f(flag: bool):
|
||||||
|
x = str() if flag else str("hello")
|
||||||
|
reveal_type(x) # revealed: Literal["", "hello"]
|
||||||
|
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
|
||||||
|
|
||||||
|
def f(x: int | str):
|
||||||
|
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
|
||||||
|
super(int, x)
|
||||||
|
```
|
||||||
|
|
||||||
|
Even when `super()` is constructed separately for each branch of a union, it should behave correctly
|
||||||
|
in all cases.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f(flag: bool):
|
||||||
|
if flag:
|
||||||
|
class A:
|
||||||
|
x = 1
|
||||||
|
y: int = 1
|
||||||
|
|
||||||
|
a: str = "hello"
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
s = super(B, B())
|
||||||
|
else:
|
||||||
|
class C:
|
||||||
|
x = 2
|
||||||
|
y: int | str = "test"
|
||||||
|
|
||||||
|
class D(C): ...
|
||||||
|
s = super(D, D())
|
||||||
|
|
||||||
|
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
|
||||||
|
|
||||||
|
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
|
||||||
|
reveal_type(s.y) # revealed: int | str
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
|
||||||
|
reveal_type(s.a) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supers with Generic Classes
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||||
|
|
||||||
|
class A[T]:
|
||||||
|
def f(self, a: T) -> T:
|
||||||
|
return a
|
||||||
|
|
||||||
|
class B[T](A[T]):
|
||||||
|
def f(self, b: T) -> T:
|
||||||
|
return super().f(b)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invalid Usages
|
||||||
|
|
||||||
|
### Unresolvable `super()` Calls
|
||||||
|
|
||||||
|
If an appropriate class and argument cannot be found, a runtime error will occur.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
reveal_type(super()) # revealed: Unknown
|
||||||
|
|
||||||
|
def f():
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
super()
|
||||||
|
|
||||||
|
# No first argument in its scope
|
||||||
|
class A:
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
s = super()
|
||||||
|
|
||||||
|
def f(self):
|
||||||
|
def g():
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
super()
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
lambda: super()
|
||||||
|
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
(super() for _ in range(10))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def h():
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
super()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failing Condition Checks
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
`super()` requires its first argument to be a valid class, and its second argument to be either an
|
||||||
|
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f(x: int):
|
||||||
|
# error: [invalid-super-argument] "`int` is not a valid class"
|
||||||
|
super(x, x)
|
||||||
|
|
||||||
|
type IntAlias = int
|
||||||
|
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
|
||||||
|
super(IntAlias, 0)
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(int, str()))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(int, str))
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, A()))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, object()))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, A))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, object))
|
||||||
|
|
||||||
|
super(object, object()).__class__
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Member Access via `super`
|
||||||
|
|
||||||
|
Accessing instance members through `super()` is not allowed.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __init__(self, a: int):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def __init__(self, a: int):
|
||||||
|
super().__init__(a)
|
||||||
|
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||||
|
super().a
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
|
||||||
|
super(B, B(42)).a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dunder Method Resolution
|
||||||
|
|
||||||
|
Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super
|
||||||
|
object itself. In other words, `super` should not be treated as if it inherits attributes of the
|
||||||
|
`owner`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __getitem__(self, key: int) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
reveal_type(A()[0]) # revealed: int
|
||||||
|
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
|
||||||
|
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
|
||||||
|
super(B, B())[0]
|
||||||
|
```
|
||||||
@@ -50,13 +50,17 @@ reveal_type(x) # revealed: LiteralString
|
|||||||
if x != "abc":
|
if x != "abc":
|
||||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||||
|
|
||||||
reveal_type(x == "abc") # revealed: Literal[False]
|
# TODO: This should be `Literal[False]`
|
||||||
reveal_type("abc" == x) # revealed: Literal[False]
|
reveal_type(x == "abc") # revealed: bool
|
||||||
|
# TODO: This should be `Literal[False]`
|
||||||
|
reveal_type("abc" == x) # revealed: bool
|
||||||
reveal_type(x == "something else") # revealed: bool
|
reveal_type(x == "something else") # revealed: bool
|
||||||
reveal_type("something else" == x) # revealed: bool
|
reveal_type("something else" == x) # revealed: bool
|
||||||
|
|
||||||
reveal_type(x != "abc") # revealed: Literal[True]
|
# TODO: This should be `Literal[True]`
|
||||||
reveal_type("abc" != x) # revealed: Literal[True]
|
reveal_type(x != "abc") # revealed: bool
|
||||||
|
# TODO: This should be `Literal[True]`
|
||||||
|
reveal_type("abc" != x) # revealed: bool
|
||||||
reveal_type(x != "something else") # revealed: bool
|
reveal_type(x != "something else") # revealed: bool
|
||||||
reveal_type("something else" != x) # revealed: bool
|
reveal_type("something else" != x) # revealed: bool
|
||||||
|
|
||||||
@@ -79,10 +83,10 @@ def _(x: int):
|
|||||||
if x != 1:
|
if x != 1:
|
||||||
reveal_type(x) # revealed: int & ~Literal[1]
|
reveal_type(x) # revealed: int & ~Literal[1]
|
||||||
|
|
||||||
reveal_type(x != 1) # revealed: Literal[True]
|
reveal_type(x != 1) # revealed: bool
|
||||||
reveal_type(x != 2) # revealed: bool
|
reveal_type(x != 2) # revealed: bool
|
||||||
|
|
||||||
reveal_type(x == 1) # revealed: Literal[False]
|
reveal_type(x == 1) # revealed: bool
|
||||||
reveal_type(x == 2) # revealed: bool
|
reveal_type(x == 2) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Pattern matching
|
# Pattern matching
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
## With wildcard
|
## With wildcard
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
731
crates/red_knot_python_semantic/resources/mdtest/dataclasses.md
Normal file
731
crates/red_knot_python_semantic/resources/mdtest/dataclasses.md
Normal file
@@ -0,0 +1,731 @@
|
|||||||
|
# Dataclasses
|
||||||
|
|
||||||
|
## Basic
|
||||||
|
|
||||||
|
Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`,
|
||||||
|
`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass`
|
||||||
|
decorator. By default, only the three mentioned methods are generated.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Person:
|
||||||
|
name: str
|
||||||
|
age: int | None = None
|
||||||
|
|
||||||
|
alice1 = Person("Alice", 30)
|
||||||
|
alice2 = Person(name="Alice", age=30)
|
||||||
|
alice3 = Person(age=30, name="Alice")
|
||||||
|
alice4 = Person("Alice", age=30)
|
||||||
|
|
||||||
|
reveal_type(alice1) # revealed: Person
|
||||||
|
reveal_type(type(alice1)) # revealed: type[Person]
|
||||||
|
|
||||||
|
reveal_type(alice1.name) # revealed: str
|
||||||
|
reveal_type(alice1.age) # revealed: int | None
|
||||||
|
|
||||||
|
reveal_type(repr(alice1)) # revealed: str
|
||||||
|
|
||||||
|
reveal_type(alice1 == alice2) # revealed: bool
|
||||||
|
reveal_type(alice1 == "Alice") # revealed: bool
|
||||||
|
|
||||||
|
bob = Person("Bob")
|
||||||
|
bob2 = Person("Bob", None)
|
||||||
|
bob3 = Person(name="Bob")
|
||||||
|
bob4 = Person(name="Bob", age=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
The signature of the `__init__` method is generated based on the classes attributes. The following
|
||||||
|
calls are not valid:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [missing-argument]
|
||||||
|
Person()
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
Person("Eve", 20, "too many arguments")
|
||||||
|
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
Person("Eve", "string instead of int")
|
||||||
|
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
Person(20, "Eve")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signature of `__init__`
|
||||||
|
|
||||||
|
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
|
||||||
|
|
||||||
|
Declarations in the class body are used to generate the signature of the `__init__` method. If the
|
||||||
|
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
|
||||||
|
the default value.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int
|
||||||
|
y: str = "default"
|
||||||
|
z: int | None = 1 + 2
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works if the declaration and binding are split:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int | None
|
||||||
|
x = None
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int | None = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-fully static types are handled correctly:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
x: Any
|
||||||
|
y: int | Any
|
||||||
|
z: tuple[int, Any]
|
||||||
|
|
||||||
|
reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Variables without annotations are ignored:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int
|
||||||
|
y = 1
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
If attributes without default values are declared after attributes with default values, a
|
||||||
|
`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int = 1
|
||||||
|
# TODO: this should be an error: field without default defined after field with default
|
||||||
|
y: str
|
||||||
|
```
|
||||||
|
|
||||||
|
Pure class attributes (`ClassVar`) are not included in the signature of `__init__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int
|
||||||
|
y: ClassVar[str] = "default"
|
||||||
|
z: bool
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None
|
||||||
|
|
||||||
|
d = D(1, True)
|
||||||
|
reveal_type(d.x) # revealed: int
|
||||||
|
reveal_type(d.y) # revealed: str
|
||||||
|
reveal_type(d.z) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Function declarations do not affect the signature of `__init__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
def y(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
And neither do nested class declarations:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
class Nested:
|
||||||
|
y: str
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
But if there is a variable annotation with a function or class literal type, the signature of
|
||||||
|
`__init__` will include this field:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import TypeOf
|
||||||
|
|
||||||
|
class SomeClass: ...
|
||||||
|
|
||||||
|
def some_function() -> None: ...
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
function_literal: TypeOf[some_function]
|
||||||
|
class_literal: TypeOf[SomeClass]
|
||||||
|
class_subtype_of: type[SomeClass]
|
||||||
|
|
||||||
|
# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None
|
||||||
|
reveal_type(D.__init__)
|
||||||
|
```
|
||||||
|
|
||||||
|
More realistically, dataclasses can have `Callable` attributes:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
c: Callable[[int], str]
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Implicit instance attributes do not affect the signature of `__init__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
def f(self, y: str) -> None:
|
||||||
|
self.y: str = y
|
||||||
|
|
||||||
|
reveal_type(D(1).y) # revealed: str
|
||||||
|
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't
|
||||||
|
be included in the signature of `__init__`. This is a case that we currently don't detect:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass
|
||||||
|
class D:
|
||||||
|
# (x) is an expression, not a "simple name"
|
||||||
|
(x): int = 1
|
||||||
|
|
||||||
|
# TODO: should ideally not include a `x` parameter
|
||||||
|
reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
## `@dataclass` calls with arguments
|
||||||
|
|
||||||
|
The `@dataclass` decorator can take several arguments to customize the existence of the generated
|
||||||
|
methods. The following test makes sure that we still treat the class as a dataclass if (the default)
|
||||||
|
arguments are passed in:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(init=True, repr=True, eq=True)
|
||||||
|
class Person:
|
||||||
|
name: str
|
||||||
|
age: int | None = None
|
||||||
|
|
||||||
|
alice = Person("Alice", 30)
|
||||||
|
reveal_type(repr(alice)) # revealed: str
|
||||||
|
reveal_type(alice == alice) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
If `init` is set to `False`, no `__init__` method is generated:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(init=False)
|
||||||
|
class C:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
C() # Okay
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
C(1)
|
||||||
|
|
||||||
|
repr(C())
|
||||||
|
|
||||||
|
C() == C()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other dataclass parameters
|
||||||
|
|
||||||
|
### `repr`
|
||||||
|
|
||||||
|
A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but
|
||||||
|
in that case `__repr__` is still available via `object.__repr__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class WithoutRepr:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
### `eq`
|
||||||
|
|
||||||
|
The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but
|
||||||
|
`__eq__` is still available via `object.__eq__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(eq=False)
|
||||||
|
class WithoutEq:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### `order`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
|
||||||
|
methods will be generated:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WithoutOrder:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
|
||||||
|
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
|
||||||
|
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
|
||||||
|
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
|
||||||
|
|
||||||
|
@dataclass(order=True)
|
||||||
|
class WithOrder:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
WithOrder(1) < WithOrder(2)
|
||||||
|
WithOrder(1) <= WithOrder(2)
|
||||||
|
WithOrder(1) > WithOrder(2)
|
||||||
|
WithOrder(1) >= WithOrder(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Comparisons are only allowed for `WithOrder` instances:
|
||||||
|
|
||||||
|
```py
|
||||||
|
WithOrder(1) < 2 # error: [unsupported-operator]
|
||||||
|
WithOrder(1) <= 2 # error: [unsupported-operator]
|
||||||
|
WithOrder(1) > 2 # error: [unsupported-operator]
|
||||||
|
WithOrder(1) >= 2 # error: [unsupported-operator]
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works for generic dataclasses:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(order=True)
|
||||||
|
class GenericWithOrder[T]:
|
||||||
|
x: T
|
||||||
|
|
||||||
|
GenericWithOrder[int](1) < GenericWithOrder[int](1)
|
||||||
|
|
||||||
|
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
|
||||||
|
```
|
||||||
|
|
||||||
|
If a class already defines one of the comparison methods, a `TypeError` is raised at runtime.
|
||||||
|
Ideally, we would emit a diagnostic in that case:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass(order=True)
|
||||||
|
class AlreadyHasCustomDunderLt:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
# TODO: Ideally, we would emit a diagnostic here
|
||||||
|
def __lt__(self, other: object) -> bool:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
### `unsafe_hash`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
### `frozen`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
### `match_args`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
### `kw_only`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
### `slots`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
### `weakref_slot`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
## Inheritance
|
||||||
|
|
||||||
|
### Normal class inheriting from a dataclass
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Base:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
class Derived(Base): ...
|
||||||
|
|
||||||
|
d = Derived(1) # OK
|
||||||
|
reveal_type(d.x) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dataclass inheriting from normal class
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
x: int = 1
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Derived(Base):
|
||||||
|
y: str
|
||||||
|
|
||||||
|
d = Derived("a")
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
Derived(1, "a")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dataclass inheriting from another dataclass
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Base:
|
||||||
|
x: int
|
||||||
|
y: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Derived(Base):
|
||||||
|
z: bool
|
||||||
|
|
||||||
|
d = Derived(1, "a", True) # OK
|
||||||
|
|
||||||
|
reveal_type(d.x) # revealed: int
|
||||||
|
reveal_type(d.y) # revealed: str
|
||||||
|
reveal_type(d.z) # revealed: bool
|
||||||
|
|
||||||
|
# error: [missing-argument]
|
||||||
|
Derived(1, "a")
|
||||||
|
|
||||||
|
# error: [missing-argument]
|
||||||
|
Derived(True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overwriting attributes from base class
|
||||||
|
|
||||||
|
The following example comes from the
|
||||||
|
[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x`
|
||||||
|
attribute appears just once in the `__init__` signature, and the default value is taken from the
|
||||||
|
derived class
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Base:
|
||||||
|
x: Any = 15.0
|
||||||
|
y: int = 0
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C(Base):
|
||||||
|
z: int = 10
|
||||||
|
x: int = 15
|
||||||
|
|
||||||
|
reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic dataclasses
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DataWithDescription[T]:
|
||||||
|
data: T
|
||||||
|
description: str
|
||||||
|
|
||||||
|
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
|
||||||
|
|
||||||
|
d_int = DataWithDescription[int](1, "description") # OK
|
||||||
|
reveal_type(d_int.data) # revealed: int
|
||||||
|
reveal_type(d_int.description) # revealed: str
|
||||||
|
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
DataWithDescription[int](None, "description")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Descriptor-typed fields
|
||||||
|
|
||||||
|
### Same type in `__get__` and `__set__`
|
||||||
|
|
||||||
|
For the following descriptor, the return type of `__get__` and the type of the `value` parameter in
|
||||||
|
`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of
|
||||||
|
the type of the descriptor), and the default value is also of this type:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class UppercaseString:
|
||||||
|
_value: str = ""
|
||||||
|
|
||||||
|
def __get__(self, instance: object, owner: None | type) -> str:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def __set__(self, instance: object, value: str) -> None:
|
||||||
|
self._value = value.upper()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
upper: UppercaseString = UppercaseString()
|
||||||
|
|
||||||
|
reveal_type(C.__init__) # revealed: (upper: str = str) -> None
|
||||||
|
|
||||||
|
c = C("abc")
|
||||||
|
reveal_type(c.upper) # revealed: str
|
||||||
|
|
||||||
|
# This is also okay:
|
||||||
|
C()
|
||||||
|
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
C(1)
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
C("a", "b")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Different types in `__get__` and `__set__`
|
||||||
|
|
||||||
|
In general, the type of the `__init__` parameter is determined by the `value` parameter type of the
|
||||||
|
`__set__` method (`str` in the example below). However, the default value is generated by calling
|
||||||
|
the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None`
|
||||||
|
for the `instance` argument.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal, overload
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class ConvertToLength:
|
||||||
|
_len: int = 0
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __get__(self, instance: None, owner: type) -> Literal[""]: ...
|
||||||
|
@overload
|
||||||
|
def __get__(self, instance: object, owner: type | None) -> int: ...
|
||||||
|
def __get__(self, instance: object | None, owner: type | None) -> str | int:
|
||||||
|
if instance is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return self._len
|
||||||
|
|
||||||
|
def __set__(self, instance, value: str) -> None:
|
||||||
|
self._len = len(value)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
converter: ConvertToLength = ConvertToLength()
|
||||||
|
|
||||||
|
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
|
||||||
|
|
||||||
|
c = C("abc")
|
||||||
|
reveal_type(c.converter) # revealed: int
|
||||||
|
|
||||||
|
# This is also okay:
|
||||||
|
C()
|
||||||
|
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
C(1)
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
C("a", "b")
|
||||||
|
```
|
||||||
|
|
||||||
|
### With overloaded `__set__` method
|
||||||
|
|
||||||
|
If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the
|
||||||
|
union of all possible `value` parameter types:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class AcceptsStrAndInt:
|
||||||
|
def __get__(self, instance, owner) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __set__(self, instance: object, value: str) -> None: ...
|
||||||
|
@overload
|
||||||
|
def __set__(self, instance: object, value: int) -> None: ...
|
||||||
|
def __set__(self, instance: object, value) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
field: AcceptsStrAndInt = AcceptsStrAndInt()
|
||||||
|
|
||||||
|
reveal_type(C.__init__) # revealed: (field: str | int = int) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
## `dataclasses.field`
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
## Other special cases
|
||||||
|
|
||||||
|
### `dataclasses.dataclass`
|
||||||
|
|
||||||
|
We also understand dataclasses if they are decorated with the fully qualified name:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class C:
|
||||||
|
x: str
|
||||||
|
|
||||||
|
reveal_type(C.__init__) # revealed: (x: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dataclass with custom `__init__` method
|
||||||
|
|
||||||
|
If a class already defines `__init__`, it is not replaced by the `dataclass` decorator.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(init=True)
|
||||||
|
class C:
|
||||||
|
x: str
|
||||||
|
|
||||||
|
def __init__(self, x: int) -> None:
|
||||||
|
self.x = str(x)
|
||||||
|
|
||||||
|
C(1) # OK
|
||||||
|
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
C("a")
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, if we set `init=False`, we still recognize the custom `__init__` method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclass(init=False)
|
||||||
|
class D:
|
||||||
|
def __init__(self, x: int) -> None:
|
||||||
|
self.x = str(x)
|
||||||
|
|
||||||
|
D(1) # OK
|
||||||
|
D() # error: [missing-argument]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing instance attributes on the class itself
|
||||||
|
|
||||||
|
Just like for normal classes, accessing instance attributes on the class itself is not allowed:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||||
|
C.x
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return type of `dataclass(...)`
|
||||||
|
|
||||||
|
A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator.
|
||||||
|
We can store the callable in a variable and later use it as a decorator:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
dataclass_with_order = dataclass(order=True)
|
||||||
|
|
||||||
|
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
|
||||||
|
|
||||||
|
@dataclass_with_order
|
||||||
|
class C:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
C(1) < C(2) # ok
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using `dataclass` as a function
|
||||||
|
|
||||||
|
To do
|
||||||
|
|
||||||
|
## Internals
|
||||||
|
|
||||||
|
The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`,
|
||||||
|
and attributes like the MRO are unchanged:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Person:
|
||||||
|
name: str
|
||||||
|
age: int | None = None
|
||||||
|
|
||||||
|
reveal_type(type(Person)) # revealed: Literal[type]
|
||||||
|
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated methods have the following signatures:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# TODO: `self` is missing here
|
||||||
|
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
|
||||||
|
|
||||||
|
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
|
||||||
|
|
||||||
|
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
|
||||||
|
```
|
||||||
@@ -145,10 +145,10 @@ def f(x: int) -> int:
|
|||||||
return x**2
|
return x**2
|
||||||
|
|
||||||
# TODO: Should be `_lru_cache_wrapper[int]`
|
# TODO: Should be `_lru_cache_wrapper[int]`
|
||||||
reveal_type(f) # revealed: @Todo(generics)
|
reveal_type(f) # revealed: @Todo(specialized non-generic class)
|
||||||
|
|
||||||
# TODO: Should be `int`
|
# TODO: Should be `int`
|
||||||
reveal_type(f(1)) # revealed: @Todo(generics)
|
reveal_type(f(1)) # revealed: @Todo(specialized non-generic class)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lambdas as decorators
|
## Lambdas as decorators
|
||||||
|
|||||||
@@ -459,11 +459,9 @@ class Descriptor:
|
|||||||
class C:
|
class C:
|
||||||
d: Descriptor = Descriptor()
|
d: Descriptor = Descriptor()
|
||||||
|
|
||||||
# TODO: should be `Literal["called on class object"]
|
reveal_type(C.d) # revealed: Literal["called on class object"]
|
||||||
reveal_type(C.d) # revealed: LiteralString
|
|
||||||
|
|
||||||
# TODO: should be `Literal["called on instance"]
|
reveal_type(C().d) # revealed: Literal["called on instance"]
|
||||||
reveal_type(C().d) # revealed: LiteralString
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Descriptor protocol for dunder methods
|
## Descriptor protocol for dunder methods
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Version-related syntax error diagnostics
|
||||||
|
|
||||||
|
## `match` statement
|
||||||
|
|
||||||
|
The `match` statement was introduced in Python 3.10.
|
||||||
|
|
||||||
|
### Before 3.10
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
We should emit a syntax error before 3.10.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.9"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
|
||||||
|
case 1:
|
||||||
|
print("it's one")
|
||||||
|
```
|
||||||
|
|
||||||
|
### After 3.10
|
||||||
|
|
||||||
|
On or after 3.10, no error should be reported.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
match 2:
|
||||||
|
case 1:
|
||||||
|
print("it's one")
|
||||||
|
```
|
||||||
@@ -26,6 +26,11 @@ def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
|
|||||||
|
|
||||||
## Use case: Type narrowing and exhaustiveness checking
|
## Use case: Type narrowing and exhaustiveness checking
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
|
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
|
||||||
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
|
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ from knot_extensions import Unknown
|
|||||||
|
|
||||||
def f(x: Any, y: Unknown, z: Any | str | int):
|
def f(x: Any, y: Unknown, z: Any | str | int):
|
||||||
a = cast(dict[str, Any], x)
|
a = cast(dict[str, Any], x)
|
||||||
reveal_type(a) # revealed: @Todo(generics)
|
reveal_type(a) # revealed: @Todo(specialized non-generic class)
|
||||||
|
|
||||||
b = cast(Any, y)
|
b = cast(Any, y)
|
||||||
reveal_type(b) # revealed: Any
|
reveal_type(b) # revealed: Any
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ def g(x: Any = "foo"):
|
|||||||
|
|
||||||
## Stub functions
|
## Stub functions
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
### In Protocol
|
### In Protocol
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ def f() -> int:
|
|||||||
|
|
||||||
### In Protocol
|
### In Protocol
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Protocol, TypeVar
|
from typing import Protocol, TypeVar
|
||||||
|
|
||||||
@@ -69,8 +74,6 @@ class Baz(Bar):
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
class Qux(Protocol[T]):
|
class Qux(Protocol[T]):
|
||||||
# TODO: no error
|
|
||||||
# error: [invalid-return-type]
|
|
||||||
def f(self) -> int: ...
|
def f(self) -> int: ...
|
||||||
|
|
||||||
class Foo(Protocol):
|
class Foo(Protocol):
|
||||||
@@ -85,6 +88,11 @@ class Lorem(t[0]):
|
|||||||
|
|
||||||
### In abstract method
|
### In abstract method
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Generic classes
|
# Generic classes
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.13"
|
||||||
|
```
|
||||||
|
|
||||||
## PEP 695 syntax
|
## PEP 695 syntax
|
||||||
|
|
||||||
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
|
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
|
||||||
@@ -40,8 +45,6 @@ from typing import Generic, TypeVar
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# error: [invalid-base]
|
|
||||||
class C(Generic[T]): ...
|
class C(Generic[T]): ...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -149,23 +152,102 @@ If a typevar does not provide a default, we use `Unknown`:
|
|||||||
reveal_type(C()) # revealed: C[Unknown]
|
reveal_type(C()) # revealed: C[Unknown]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Inferring generic class parameters from constructors
|
||||||
|
|
||||||
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
||||||
parameter:
|
parameter. The types inferred from a type context and from a constructor parameter must be
|
||||||
|
consistent with each other.
|
||||||
|
|
||||||
|
## `__new__` only
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class E[T]:
|
class C[T]:
|
||||||
def __init__(self, x: T) -> None: ...
|
def __new__(cls, x: T) -> "C[T]":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
# TODO: revealed: E[int] or E[Literal[1]]
|
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||||
reveal_type(E(1)) # revealed: E[Unknown]
|
|
||||||
|
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||||
|
wrong_innards: C[int] = C("five")
|
||||||
```
|
```
|
||||||
|
|
||||||
The types inferred from a type context and from a constructor parameter must be consistent with each
|
## `__init__` only
|
||||||
other:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: error: [invalid-argument-type]
|
class C[T]:
|
||||||
wrong_innards: E[int] = E("five")
|
def __init__(self, x: T) -> None: ...
|
||||||
|
|
||||||
|
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||||
|
wrong_innards: C[int] = C("five")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Identical `__new__` and `__init__` signatures
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C[T]:
|
||||||
|
def __new__(cls, x: T) -> "C[T]":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, x: T) -> None: ...
|
||||||
|
|
||||||
|
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||||
|
wrong_innards: C[int] = C("five")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatible `__new__` and `__init__` signatures
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C[T]:
|
||||||
|
def __new__(cls, *args, **kwargs) -> "C[T]":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, x: T) -> None: ...
|
||||||
|
|
||||||
|
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||||
|
wrong_innards: C[int] = C("five")
|
||||||
|
|
||||||
|
class D[T]:
|
||||||
|
def __new__(cls, x: T) -> "D[T]":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None: ...
|
||||||
|
|
||||||
|
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
|
||||||
|
wrong_innards: D[int] = D("five")
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__init__` is itself generic
|
||||||
|
|
||||||
|
TODO: These do not currently work yet, because we don't correctly model the nested generic contexts.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C[T]:
|
||||||
|
def __init__[S](self, x: T, y: S) -> None: ...
|
||||||
|
|
||||||
|
# TODO: no error
|
||||||
|
# TODO: revealed: C[Literal[1]]
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
reveal_type(C(1, 1)) # revealed: C[Unknown]
|
||||||
|
# TODO: no error
|
||||||
|
# TODO: revealed: C[Literal[1]]
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
reveal_type(C(1, "string")) # revealed: C[Unknown]
|
||||||
|
# TODO: no error
|
||||||
|
# TODO: revealed: C[Literal[1]]
|
||||||
|
# error: [invalid-argument-type]
|
||||||
|
reveal_type(C(1, True)) # revealed: C[Unknown]
|
||||||
|
|
||||||
|
# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||||
|
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`"
|
||||||
|
wrong_innards: C[int] = C("five", 1)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generic subclass
|
## Generic subclass
|
||||||
@@ -200,10 +282,7 @@ class C[T]:
|
|||||||
def cannot_shadow_class_typevar[T](self, t: T): ...
|
def cannot_shadow_class_typevar[T](self, t: T): ...
|
||||||
|
|
||||||
c: C[int] = C[int]()
|
c: C[int] = C[int]()
|
||||||
# TODO: no error
|
reveal_type(c.method("string")) # revealed: Literal["string"]
|
||||||
# TODO: revealed: str or Literal["string"]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(c.method("string")) # revealed: U
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cyclic class definition
|
## Cyclic class definition
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Generic functions
|
# Generic functions
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
## Typevar must be used at least twice
|
## Typevar must be used at least twice
|
||||||
|
|
||||||
If you're only using a typevar for a single parameter, you don't need the typevar — just use
|
If you're only using a typevar for a single parameter, you don't need the typevar — just use
|
||||||
@@ -43,33 +48,14 @@ def absurd[T]() -> T:
|
|||||||
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
|
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
|
||||||
is bound to at each call site.
|
is bound to at each call site.
|
||||||
|
|
||||||
TODO: Note that some of the TODO revealed types have two options, since we haven't decided yet
|
|
||||||
whether we want to infer a more specific `Literal` type where possible, or use heuristics to weaken
|
|
||||||
the inferred type to e.g. `int`.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def f[T](x: T) -> T:
|
def f[T](x: T) -> T:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
# TODO: no error
|
reveal_type(f(1)) # revealed: Literal[1]
|
||||||
# TODO: revealed: int or Literal[1]
|
reveal_type(f(1.0)) # revealed: float
|
||||||
# error: [invalid-argument-type]
|
reveal_type(f(True)) # revealed: Literal[True]
|
||||||
reveal_type(f(1)) # revealed: T
|
reveal_type(f("string")) # revealed: Literal["string"]
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: float
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(f(1.0)) # revealed: T
|
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: bool or Literal[true]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(f(True)) # revealed: T
|
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: str or Literal["string"]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(f("string")) # revealed: T
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inferring “deep” generic parameter types
|
## Inferring “deep” generic parameter types
|
||||||
@@ -82,7 +68,7 @@ def f[T](x: list[T]) -> T:
|
|||||||
return x[0]
|
return x[0]
|
||||||
|
|
||||||
# TODO: revealed: float
|
# TODO: revealed: float
|
||||||
reveal_type(f([1.0, 2.0])) # revealed: T
|
reveal_type(f([1.0, 2.0])) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
## Typevar constraints
|
## Typevar constraints
|
||||||
@@ -93,7 +79,6 @@ in the function.
|
|||||||
|
|
||||||
```py
|
```py
|
||||||
def good_param[T: int](x: T) -> None:
|
def good_param[T: int](x: T) -> None:
|
||||||
# TODO: revealed: T & int
|
|
||||||
reveal_type(x) # revealed: T
|
reveal_type(x) # revealed: T
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -162,61 +147,41 @@ parameters simultaneously.
|
|||||||
def two_params[T](x: T, y: T) -> T:
|
def two_params[T](x: T, y: T) -> T:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
# TODO: no error
|
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
|
||||||
# TODO: revealed: str
|
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
|
||||||
# error: [invalid-argument-type]
|
```
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(two_params("a", "b")) # revealed: T
|
|
||||||
|
|
||||||
# TODO: no error
|
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
|
||||||
# TODO: revealed: str | int
|
all of the constraints.
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
```py
|
||||||
reveal_type(two_params("a", 1)) # revealed: T
|
def union_param[T](x: T | None) -> T:
|
||||||
|
if x is None:
|
||||||
|
raise ValueError
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(union_param("a")) # revealed: Literal["a"]
|
||||||
|
reveal_type(union_param(1)) # revealed: Literal[1]
|
||||||
|
reveal_type(union_param(None)) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def param_with_union[T](x: T | int, y: T) -> T:
|
def union_and_nonunion_params[T](x: T | int, y: T) -> T:
|
||||||
return y
|
return y
|
||||||
|
|
||||||
# TODO: no error
|
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
|
||||||
# TODO: revealed: str
|
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
|
||||||
# error: [invalid-argument-type]
|
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
|
||||||
reveal_type(param_with_union(1, "a")) # revealed: T
|
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
|
||||||
|
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: str
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(param_with_union("a", "a")) # revealed: T
|
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: int
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(param_with_union(1, 1)) # revealed: T
|
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: str | int
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(param_with_union("a", 1)) # revealed: T
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
|
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
|
||||||
return y
|
return y
|
||||||
|
|
||||||
# TODO: no error
|
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||||
# TODO: revealed: tuple[str, int]
|
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[T, S]
|
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: tuple[str, int]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[T, S]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inferring nested generic function calls
|
## Inferring nested generic function calls
|
||||||
@@ -231,15 +196,6 @@ def f[T](x: T) -> tuple[T, int]:
|
|||||||
def g[T](x: T) -> T | None:
|
def g[T](x: T) -> T | None:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
# TODO: no error
|
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
|
||||||
# TODO: revealed: tuple[str | None, int]
|
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(f(g("a"))) # revealed: tuple[T, int]
|
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: tuple[str, int] | None
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(g(f("a"))) # revealed: T | None
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# PEP 695 Generics
|
# PEP 695 Generics
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
|
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
|
||||||
|
|
||||||
## Type variables
|
## Type variables
|
||||||
@@ -59,19 +64,19 @@ is.)
|
|||||||
from knot_extensions import is_fully_static, static_assert
|
from knot_extensions import is_fully_static, static_assert
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
def unbounded_unconstrained[T](t: list[T]) -> None:
|
def unbounded_unconstrained[T](t: T) -> None:
|
||||||
static_assert(is_fully_static(T))
|
static_assert(is_fully_static(T))
|
||||||
|
|
||||||
def bounded[T: int](t: list[T]) -> None:
|
def bounded[T: int](t: T) -> None:
|
||||||
static_assert(is_fully_static(T))
|
static_assert(is_fully_static(T))
|
||||||
|
|
||||||
def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||||
static_assert(not is_fully_static(T))
|
static_assert(not is_fully_static(T))
|
||||||
|
|
||||||
def constrained[T: (int, str)](t: list[T]) -> None:
|
def constrained[T: (int, str)](t: T) -> None:
|
||||||
static_assert(is_fully_static(T))
|
static_assert(is_fully_static(T))
|
||||||
|
|
||||||
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
|
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
|
||||||
static_assert(not is_fully_static(T))
|
static_assert(not is_fully_static(T))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -94,7 +99,7 @@ class Base(Super): ...
|
|||||||
class Sub(Base): ...
|
class Sub(Base): ...
|
||||||
class Unrelated: ...
|
class Unrelated: ...
|
||||||
|
|
||||||
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
|
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
|
||||||
static_assert(is_assignable_to(T, T))
|
static_assert(is_assignable_to(T, T))
|
||||||
static_assert(is_assignable_to(T, object))
|
static_assert(is_assignable_to(T, object))
|
||||||
static_assert(not is_assignable_to(T, Super))
|
static_assert(not is_assignable_to(T, Super))
|
||||||
@@ -124,7 +129,7 @@ is a final class, since the typevar can still be specialized to `Never`.)
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from typing_extensions import final
|
from typing_extensions import final
|
||||||
|
|
||||||
def bounded[T: Super](t: list[T]) -> None:
|
def bounded[T: Super](t: T) -> None:
|
||||||
static_assert(is_assignable_to(T, Super))
|
static_assert(is_assignable_to(T, Super))
|
||||||
static_assert(not is_assignable_to(T, Sub))
|
static_assert(not is_assignable_to(T, Sub))
|
||||||
static_assert(not is_assignable_to(Super, T))
|
static_assert(not is_assignable_to(Super, T))
|
||||||
@@ -135,7 +140,7 @@ def bounded[T: Super](t: list[T]) -> None:
|
|||||||
static_assert(not is_subtype_of(Super, T))
|
static_assert(not is_subtype_of(Super, T))
|
||||||
static_assert(not is_subtype_of(Sub, T))
|
static_assert(not is_subtype_of(Sub, T))
|
||||||
|
|
||||||
def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||||
static_assert(is_assignable_to(T, Any))
|
static_assert(is_assignable_to(T, Any))
|
||||||
static_assert(is_assignable_to(Any, T))
|
static_assert(is_assignable_to(Any, T))
|
||||||
static_assert(is_assignable_to(T, Super))
|
static_assert(is_assignable_to(T, Super))
|
||||||
@@ -153,7 +158,7 @@ def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
|||||||
@final
|
@final
|
||||||
class FinalClass: ...
|
class FinalClass: ...
|
||||||
|
|
||||||
def bounded_final[T: FinalClass](t: list[T]) -> None:
|
def bounded_final[T: FinalClass](t: T) -> None:
|
||||||
static_assert(is_assignable_to(T, FinalClass))
|
static_assert(is_assignable_to(T, FinalClass))
|
||||||
static_assert(not is_assignable_to(FinalClass, T))
|
static_assert(not is_assignable_to(FinalClass, T))
|
||||||
|
|
||||||
@@ -167,14 +172,14 @@ true even if both typevars are bounded by the same final class, since you can sp
|
|||||||
typevars to `Never` in addition to that final class.
|
typevars to `Never` in addition to that final class.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
|
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
|
||||||
static_assert(not is_assignable_to(T, U))
|
static_assert(not is_assignable_to(T, U))
|
||||||
static_assert(not is_assignable_to(U, T))
|
static_assert(not is_assignable_to(U, T))
|
||||||
|
|
||||||
static_assert(not is_subtype_of(T, U))
|
static_assert(not is_subtype_of(T, U))
|
||||||
static_assert(not is_subtype_of(U, T))
|
static_assert(not is_subtype_of(U, T))
|
||||||
|
|
||||||
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
|
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
|
||||||
static_assert(not is_assignable_to(T, U))
|
static_assert(not is_assignable_to(T, U))
|
||||||
static_assert(not is_assignable_to(U, T))
|
static_assert(not is_assignable_to(U, T))
|
||||||
|
|
||||||
@@ -189,7 +194,7 @@ intersection of all of its constraints is a subtype of the typevar.
|
|||||||
```py
|
```py
|
||||||
from knot_extensions import Intersection
|
from knot_extensions import Intersection
|
||||||
|
|
||||||
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
|
def constrained[T: (Base, Unrelated)](t: T) -> None:
|
||||||
static_assert(not is_assignable_to(T, Super))
|
static_assert(not is_assignable_to(T, Super))
|
||||||
static_assert(not is_assignable_to(T, Base))
|
static_assert(not is_assignable_to(T, Base))
|
||||||
static_assert(not is_assignable_to(T, Sub))
|
static_assert(not is_assignable_to(T, Sub))
|
||||||
@@ -214,7 +219,7 @@ def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
|
|||||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||||
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
|
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
|
||||||
|
|
||||||
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
|
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
|
||||||
static_assert(is_assignable_to(T, Super))
|
static_assert(is_assignable_to(T, Super))
|
||||||
static_assert(is_assignable_to(T, Base))
|
static_assert(is_assignable_to(T, Base))
|
||||||
static_assert(not is_assignable_to(T, Sub))
|
static_assert(not is_assignable_to(T, Sub))
|
||||||
@@ -256,7 +261,7 @@ distinct constraints, meaning that there is (still) no guarantee that they will
|
|||||||
the same type.
|
the same type.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
|
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
|
||||||
static_assert(not is_assignable_to(T, U))
|
static_assert(not is_assignable_to(T, U))
|
||||||
static_assert(not is_assignable_to(U, T))
|
static_assert(not is_assignable_to(U, T))
|
||||||
|
|
||||||
@@ -266,7 +271,7 @@ def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> Non
|
|||||||
@final
|
@final
|
||||||
class AnotherFinalClass: ...
|
class AnotherFinalClass: ...
|
||||||
|
|
||||||
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
|
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
|
||||||
static_assert(not is_assignable_to(T, U))
|
static_assert(not is_assignable_to(T, U))
|
||||||
static_assert(not is_assignable_to(U, T))
|
static_assert(not is_assignable_to(U, T))
|
||||||
|
|
||||||
@@ -285,7 +290,7 @@ non-singleton type.
|
|||||||
```py
|
```py
|
||||||
from knot_extensions import is_singleton, is_single_valued, static_assert
|
from knot_extensions import is_singleton, is_single_valued, static_assert
|
||||||
|
|
||||||
def unbounded_unconstrained[T](t: list[T]) -> None:
|
def unbounded_unconstrained[T](t: T) -> None:
|
||||||
static_assert(not is_singleton(T))
|
static_assert(not is_singleton(T))
|
||||||
static_assert(not is_single_valued(T))
|
static_assert(not is_single_valued(T))
|
||||||
```
|
```
|
||||||
@@ -294,7 +299,7 @@ A bounded typevar is not a singleton, even if its bound is a singleton, since it
|
|||||||
specialized to `Never`.
|
specialized to `Never`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def bounded[T: None](t: list[T]) -> None:
|
def bounded[T: None](t: T) -> None:
|
||||||
static_assert(not is_singleton(T))
|
static_assert(not is_singleton(T))
|
||||||
static_assert(not is_single_valued(T))
|
static_assert(not is_single_valued(T))
|
||||||
```
|
```
|
||||||
@@ -305,14 +310,14 @@ specialize a constrained typevar to a subtype of a constraint.)
|
|||||||
```py
|
```py
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
|
def constrained_non_singletons[T: (int, str)](t: T) -> None:
|
||||||
static_assert(not is_singleton(T))
|
static_assert(not is_singleton(T))
|
||||||
static_assert(not is_single_valued(T))
|
static_assert(not is_single_valued(T))
|
||||||
|
|
||||||
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
|
def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None:
|
||||||
static_assert(is_singleton(T))
|
static_assert(is_singleton(T))
|
||||||
|
|
||||||
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
|
def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None:
|
||||||
static_assert(is_single_valued(T))
|
static_assert(is_single_valued(T))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -507,6 +512,20 @@ def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
|||||||
reveal_type(x) # revealed: T & Any
|
reveal_type(x) # revealed: T & Any
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The intersection of a typevar with any other type is assignable to (and if fully static, a subtype
|
||||||
|
of) itself.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not
|
||||||
|
|
||||||
|
def intersection_is_assignable[T](t: T) -> None:
|
||||||
|
static_assert(is_assignable_to(Intersection[T, None], T))
|
||||||
|
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(Intersection[T, None], T))
|
||||||
|
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
|
||||||
|
```
|
||||||
|
|
||||||
## Narrowing
|
## Narrowing
|
||||||
|
|
||||||
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
|
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Scoping rules for type variables
|
# Scoping rules for type variables
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
|
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
|
||||||
spec.
|
spec.
|
||||||
|
|
||||||
@@ -59,14 +64,8 @@ to a different type each time.
|
|||||||
def f[T](x: T) -> T:
|
def f[T](x: T) -> T:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
# TODO: no error
|
reveal_type(f(1)) # revealed: Literal[1]
|
||||||
# TODO: revealed: int or Literal[1]
|
reveal_type(f("a")) # revealed: Literal["a"]
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(f(1)) # revealed: T
|
|
||||||
# TODO: no error
|
|
||||||
# TODO: revealed: str or Literal["a"]
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(f("a")) # revealed: T
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Methods can mention class typevars
|
## Methods can mention class typevars
|
||||||
@@ -138,8 +137,6 @@ from typing import TypeVar, Generic
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
S = TypeVar("S")
|
S = TypeVar("S")
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# error: [invalid-base]
|
|
||||||
class Legacy(Generic[T]):
|
class Legacy(Generic[T]):
|
||||||
def m(self, x: T, y: S) -> S:
|
def m(self, x: T, y: S) -> S:
|
||||||
return y
|
return y
|
||||||
@@ -157,10 +154,7 @@ class C[T]:
|
|||||||
return y
|
return y
|
||||||
|
|
||||||
c: C[int] = C()
|
c: C[int] = C()
|
||||||
# TODO: no errors
|
reveal_type(c.m(1, "string")) # revealed: Literal["string"]
|
||||||
# TODO: revealed: str
|
|
||||||
# error: [invalid-argument-type]
|
|
||||||
reveal_type(c.m(1, "string")) # revealed: S
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Unbound typevars
|
## Unbound typevars
|
||||||
@@ -178,13 +172,11 @@ S = TypeVar("S")
|
|||||||
|
|
||||||
def f(x: T) -> None:
|
def f(x: T) -> None:
|
||||||
x: list[T] = []
|
x: list[T] = []
|
||||||
# TODO: error
|
# TODO: invalid-assignment error
|
||||||
y: list[S] = []
|
y: list[S] = []
|
||||||
|
|
||||||
# TODO: no error
|
|
||||||
# error: [invalid-base]
|
|
||||||
class C(Generic[T]):
|
class C(Generic[T]):
|
||||||
# TODO: error
|
# TODO: error: cannot use S if it's not in the current generic context
|
||||||
x: list[S] = []
|
x: list[S] = []
|
||||||
|
|
||||||
# This is not an error, as shown in the previous test
|
# This is not an error, as shown in the previous test
|
||||||
@@ -204,11 +196,11 @@ S = TypeVar("S")
|
|||||||
|
|
||||||
def f[T](x: T) -> None:
|
def f[T](x: T) -> None:
|
||||||
x: list[T] = []
|
x: list[T] = []
|
||||||
# TODO: error
|
# TODO: invalid assignment error
|
||||||
y: list[S] = []
|
y: list[S] = []
|
||||||
|
|
||||||
class C[T]:
|
class C[T]:
|
||||||
# TODO: error
|
# TODO: error: cannot use S if it's not in the current generic context
|
||||||
x: list[S] = []
|
x: list[S] = []
|
||||||
|
|
||||||
def m1(self, x: S) -> S:
|
def m1(self, x: S) -> S:
|
||||||
@@ -263,8 +255,7 @@ def f[T](x: T, y: T) -> None:
|
|||||||
class Ok[S]: ...
|
class Ok[S]: ...
|
||||||
# TODO: error for reuse of typevar
|
# TODO: error for reuse of typevar
|
||||||
class Bad1[T]: ...
|
class Bad1[T]: ...
|
||||||
# TODO: no non-subscriptable error, error for reuse of typevar
|
# TODO: error for reuse of typevar
|
||||||
# error: [non-subscriptable]
|
|
||||||
class Bad2(Iterable[T]): ...
|
class Bad2(Iterable[T]): ...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -277,8 +268,7 @@ class C[T]:
|
|||||||
class Ok1[S]: ...
|
class Ok1[S]: ...
|
||||||
# TODO: error for reuse of typevar
|
# TODO: error for reuse of typevar
|
||||||
class Bad1[T]: ...
|
class Bad1[T]: ...
|
||||||
# TODO: no non-subscriptable error, error for reuse of typevar
|
# TODO: error for reuse of typevar
|
||||||
# error: [non-subscriptable]
|
|
||||||
class Bad2(Iterable[T]): ...
|
class Bad2(Iterable[T]): ...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -292,7 +282,7 @@ class C[T]:
|
|||||||
ok1: list[T] = []
|
ok1: list[T] = []
|
||||||
|
|
||||||
class Bad:
|
class Bad:
|
||||||
# TODO: error
|
# TODO: error: cannot refer to T in nested scope
|
||||||
bad: list[T] = []
|
bad: list[T] = []
|
||||||
|
|
||||||
class Inner[S]: ...
|
class Inner[S]: ...
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Variance
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
Type variables have a property called _variance_ that affects the subtyping and assignability
|
||||||
|
relations. Much more detail can be found in the [spec]. To summarize, each typevar is either
|
||||||
|
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
|
||||||
|
currently mentioned in the typing spec, but is a fourth case that we must consider.)
|
||||||
|
|
||||||
|
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar
|
||||||
|
`C[T]`, and two types `A` and `B`.
|
||||||
|
|
||||||
|
## Covariance
|
||||||
|
|
||||||
|
With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`.
|
||||||
|
|
||||||
|
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
|
||||||
|
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
|
||||||
|
get from the sequence is a valid `int`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
class C[T]:
|
||||||
|
def receive(self) -> T:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_assignable_to(C[B], C[A]))
|
||||||
|
static_assert(not is_assignable_to(C[A], C[B]))
|
||||||
|
static_assert(is_assignable_to(C[A], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[B], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[A]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_subtype_of(C[B], C[A]))
|
||||||
|
static_assert(not is_subtype_of(C[A], C[B]))
|
||||||
|
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contravariance
|
||||||
|
|
||||||
|
With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`.
|
||||||
|
|
||||||
|
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
|
||||||
|
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
|
||||||
|
that you pass into the consumer is a valid `int`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
class C[T]:
|
||||||
|
def send(self, value: T): ...
|
||||||
|
|
||||||
|
static_assert(not is_assignable_to(C[B], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_assignable_to(C[A], C[B]))
|
||||||
|
static_assert(is_assignable_to(C[A], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[B], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[A]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(C[B], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_subtype_of(C[A], C[B]))
|
||||||
|
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invariance
|
||||||
|
|
||||||
|
With an invariant typevar, _no_ specializations of the generic class are subtypes of each other.
|
||||||
|
|
||||||
|
This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
|
||||||
|
Iterating over the elements in a list would work with a covariant typevar, just like with the
|
||||||
|
"producer" type above. Appending elements to a list would work with a contravariant typevar, just
|
||||||
|
like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant
|
||||||
|
at the same time!
|
||||||
|
|
||||||
|
If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list
|
||||||
|
of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list
|
||||||
|
would no longer only contain elements that are subtypes of `bool`.
|
||||||
|
|
||||||
|
Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a
|
||||||
|
mutable list of `int`s, since you might try to extract elements from the list: you expect every
|
||||||
|
element that you extract to be a subtype of `bool`, but the list can contain any `int`.
|
||||||
|
|
||||||
|
In the end, if you expect a mutable list, you must always be given a list of exactly that type,
|
||||||
|
since we can't know in advance which of the allowed methods you'll want to use.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
class C[T]:
|
||||||
|
def send(self, value: T): ...
|
||||||
|
def receive(self) -> T:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
static_assert(not is_assignable_to(C[B], C[A]))
|
||||||
|
static_assert(not is_assignable_to(C[A], C[B]))
|
||||||
|
static_assert(is_assignable_to(C[A], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[B], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[A]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(C[B], C[A]))
|
||||||
|
static_assert(not is_subtype_of(C[A], C[B]))
|
||||||
|
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bivariance
|
||||||
|
|
||||||
|
With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact,
|
||||||
|
equivalent to) each other.
|
||||||
|
|
||||||
|
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
|
||||||
|
at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_
|
||||||
|
the typevar was used.)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
class C[T]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_assignable_to(C[B], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_assignable_to(C[A], C[B]))
|
||||||
|
static_assert(is_assignable_to(C[A], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[B], C[Any]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[A]))
|
||||||
|
static_assert(is_assignable_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_subtype_of(C[B], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_subtype_of(C[A], C[B]))
|
||||||
|
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||||
|
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_equivalent_to(C[B], C[B]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_equivalent_to(C[B], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_equivalent_to(C[A], C[B]))
|
||||||
|
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||||
|
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||||
|
|
||||||
|
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_gradual_equivalent_to(C[B], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_gradual_equivalent_to(C[A], C[B]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_gradual_equivalent_to(C[A], C[Any]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_gradual_equivalent_to(C[B], C[Any]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[A]))
|
||||||
|
# TODO: no error
|
||||||
|
# error: [static-assert-error]
|
||||||
|
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
|
||||||
|
```
|
||||||
|
|
||||||
|
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||||
@@ -122,6 +122,11 @@ from c import Y # error: [unresolved-import]
|
|||||||
|
|
||||||
## Esoteric definitions and redefinintions
|
## Esoteric definitions and redefinintions
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
We understand all public symbols defined in an external module as being imported by a `*` import,
|
We understand all public symbols defined in an external module as being imported by a `*` import,
|
||||||
not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section
|
not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section
|
||||||
provides tests for definitions, and redefinitions, that use more esoteric AST nodes.
|
provides tests for definitions, and redefinitions, that use more esoteric AST nodes.
|
||||||
@@ -626,6 +631,30 @@ reveal_type(X) # revealed: Unknown
|
|||||||
reveal_type(Y) # revealed: bool
|
reveal_type(Y) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### An implicit import in a `.pyi` file later overridden by another assignment
|
||||||
|
|
||||||
|
`a.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
X: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
`b.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from a import X
|
||||||
|
|
||||||
|
X: bool = False
|
||||||
|
```
|
||||||
|
|
||||||
|
`c.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from b import *
|
||||||
|
|
||||||
|
reveal_type(X) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
## Visibility constraints
|
## Visibility constraints
|
||||||
|
|
||||||
If an `importer` module contains a `from exporter import *` statement in its global namespace, the
|
If an `importer` module contains a `from exporter import *` statement in its global namespace, the
|
||||||
@@ -865,15 +894,10 @@ from exporter import *
|
|||||||
|
|
||||||
reveal_type(X) # revealed: bool
|
reveal_type(X) # revealed: bool
|
||||||
|
|
||||||
# TODO none of these should error, should all reveal `bool`
|
reveal_type(_private) # revealed: bool
|
||||||
# error: [unresolved-reference]
|
reveal_type(__protected) # revealed: bool
|
||||||
reveal_type(_private) # revealed: Unknown
|
reveal_type(__dunder__) # revealed: bool
|
||||||
# error: [unresolved-reference]
|
reveal_type(___thunder___) # revealed: bool
|
||||||
reveal_type(__protected) # revealed: Unknown
|
|
||||||
# error: [unresolved-reference]
|
|
||||||
reveal_type(__dunder__) # revealed: Unknown
|
|
||||||
# error: [unresolved-reference]
|
|
||||||
reveal_type(___thunder___) # revealed: Unknown
|
|
||||||
|
|
||||||
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
|
||||||
reveal_type(Y) # revealed: bool
|
reveal_type(Y) # revealed: bool
|
||||||
@@ -1072,6 +1096,44 @@ reveal_type(Y) # revealed: bool
|
|||||||
reveal_type(Z) # revealed: Unknown
|
reveal_type(Z) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `__all__` conditionally defined in a statically known branch (2)
|
||||||
|
|
||||||
|
The same example again, but with a different `python-version` set:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
|
`exporter.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import sys
|
||||||
|
|
||||||
|
X: bool = True
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
__all__ = ["X", "Y"]
|
||||||
|
Y: bool = True
|
||||||
|
else:
|
||||||
|
__all__ = ("Z",)
|
||||||
|
Z: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
`importer.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from exporter import *
|
||||||
|
|
||||||
|
# TODO: should reveal `Unknown` and emit `[unresolved-reference]`
|
||||||
|
reveal_type(X) # revealed: bool
|
||||||
|
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(Y) # revealed: Unknown
|
||||||
|
|
||||||
|
reveal_type(Z) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
### `__all__` conditionally mutated in a statically known branch
|
### `__all__` conditionally mutated in a statically known branch
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -1084,11 +1146,11 @@ python-version = "3.11"
|
|||||||
```py
|
```py
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
__all__ = ["X"]
|
__all__ = []
|
||||||
X: bool = True
|
X: bool = True
|
||||||
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
__all__.append("Y")
|
__all__.extend(["X", "Y"])
|
||||||
Y: bool = True
|
Y: bool = True
|
||||||
else:
|
else:
|
||||||
__all__.append("Z")
|
__all__.append("Z")
|
||||||
@@ -1107,6 +1169,45 @@ reveal_type(Y) # revealed: bool
|
|||||||
reveal_type(Z) # revealed: Unknown
|
reveal_type(Z) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `__all__` conditionally mutated in a statically known branch (2)
|
||||||
|
|
||||||
|
The same example again, but with a different `python-version` set:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
|
`exporter.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
|
X: bool = True
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
__all__.extend(["X", "Y"])
|
||||||
|
Y: bool = True
|
||||||
|
else:
|
||||||
|
__all__.append("Z")
|
||||||
|
Z: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
`importer.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from exporter import *
|
||||||
|
|
||||||
|
# TODO: should reveal `Unknown` & emit `[unresolved-reference]
|
||||||
|
reveal_type(X) # revealed: bool
|
||||||
|
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
reveal_type(Y) # revealed: Unknown
|
||||||
|
|
||||||
|
reveal_type(Z) # revealed: bool
|
||||||
|
```
|
||||||
|
|
||||||
### Empty `__all__`
|
### Empty `__all__`
|
||||||
|
|
||||||
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
|
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
|
||||||
@@ -1166,6 +1267,7 @@ from b import *
|
|||||||
|
|
||||||
# TODO: should not error, should reveal `bool`
|
# TODO: should not error, should reveal `bool`
|
||||||
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
|
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
|
||||||
|
# See https://github.com/astral-sh/ruff/issues/16159
|
||||||
#
|
#
|
||||||
# error: [unresolved-reference]
|
# error: [unresolved-reference]
|
||||||
reveal_type(X) # revealed: Unknown
|
reveal_type(X) # revealed: Unknown
|
||||||
|
|||||||
@@ -842,7 +842,7 @@ def unknown(
|
|||||||
|
|
||||||
### Mixed dynamic types
|
### Mixed dynamic types
|
||||||
|
|
||||||
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
|
Gradually-equivalent types can be simplified out of intersections:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -854,10 +854,10 @@ def mixed(
|
|||||||
i3: Intersection[Not[Any], Unknown],
|
i3: Intersection[Not[Any], Unknown],
|
||||||
i4: Intersection[Not[Any], Not[Unknown]],
|
i4: Intersection[Not[Any], Not[Unknown]],
|
||||||
) -> None:
|
) -> None:
|
||||||
reveal_type(i1) # revealed: Any & Unknown
|
reveal_type(i1) # revealed: Any
|
||||||
reveal_type(i2) # revealed: Any & Unknown
|
reveal_type(i2) # revealed: Any
|
||||||
reveal_type(i3) # revealed: Any & Unknown
|
reveal_type(i3) # revealed: Any
|
||||||
reveal_type(i4) # revealed: Any & Unknown
|
reveal_type(i4) # revealed: Any
|
||||||
```
|
```
|
||||||
|
|
||||||
## Invalid
|
## Invalid
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ reveal_type(A.__class__) # revealed: type[Unknown]
|
|||||||
|
|
||||||
## PEP 695 generic
|
## PEP 695 generic
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class M(type): ...
|
class M(type): ...
|
||||||
class A[T: str](metaclass=M): ...
|
class A[T: str](metaclass=M): ...
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Narrowing with assert statements
|
||||||
|
|
||||||
|
## `assert` a value `is None` or `is not None`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(x: str | None, y: str | None):
|
||||||
|
assert x is not None
|
||||||
|
reveal_type(x) # revealed: str
|
||||||
|
assert y is None
|
||||||
|
reveal_type(y) # revealed: None
|
||||||
|
```
|
||||||
|
|
||||||
|
## `assert` a value is truthy or falsy
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(x: bool, y: bool):
|
||||||
|
assert x
|
||||||
|
reveal_type(x) # revealed: Literal[True]
|
||||||
|
assert not y
|
||||||
|
reveal_type(y) # revealed: Literal[False]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `assert` with `is` and `==` for literals
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
|
||||||
|
assert x is 2
|
||||||
|
reveal_type(x) # revealed: Literal[2]
|
||||||
|
assert y == 2
|
||||||
|
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `assert` with `isinstance`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(x: int | str):
|
||||||
|
assert isinstance(x, int)
|
||||||
|
reveal_type(x) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
## `assert` a value `in` a tuple
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
|
||||||
|
assert x in (1, 2)
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
|
assert y not in (1, 2)
|
||||||
|
reveal_type(y) # revealed: Literal[3]
|
||||||
|
```
|
||||||
@@ -223,3 +223,15 @@ def _(x: str | None, y: str | None):
|
|||||||
if y is not x:
|
if y is not x:
|
||||||
reveal_type(y) # revealed: str | None
|
reveal_type(y) # revealed: str | None
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if x := f():
|
||||||
|
reveal_type(x) # revealed: Literal[True]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[False]
|
||||||
|
```
|
||||||
|
|||||||
@@ -47,3 +47,16 @@ def _(flag1: bool, flag2: bool):
|
|||||||
# TODO should be Never
|
# TODO should be Never
|
||||||
reveal_type(x) # revealed: Literal[1, 2]
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f() -> int | str | None: ...
|
||||||
|
|
||||||
|
if isinstance(x := f(), int):
|
||||||
|
reveal_type(x) # revealed: int
|
||||||
|
elif isinstance(x, str):
|
||||||
|
reveal_type(x) # revealed: str & ~int
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: None
|
||||||
|
```
|
||||||
|
|||||||
@@ -78,3 +78,17 @@ def _(x: Literal[1, "a", "b", "c", "d"]):
|
|||||||
else:
|
else:
|
||||||
reveal_type(x) # revealed: Literal[1, "d"]
|
reveal_type(x) # revealed: Literal[1, "d"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def f() -> Literal[1, 2, 3]:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if (x := f()) in (1,):
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
```
|
||||||
|
|||||||
@@ -100,3 +100,16 @@ def _(flag: bool):
|
|||||||
else:
|
else:
|
||||||
reveal_type(x) # revealed: Literal[42]
|
reveal_type(x) # revealed: Literal[42]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def f() -> Literal[1, 2] | None: ...
|
||||||
|
|
||||||
|
if (x := f()) is None:
|
||||||
|
reveal_type(x) # revealed: None
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
|
```
|
||||||
|
|||||||
@@ -82,3 +82,14 @@ def _(x_flag: bool, y_flag: bool):
|
|||||||
reveal_type(x) # revealed: bool
|
reveal_type(x) # revealed: bool
|
||||||
reveal_type(y) # revealed: bool
|
reveal_type(y) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f() -> int | str | None: ...
|
||||||
|
|
||||||
|
if (x := f()) is not None:
|
||||||
|
reveal_type(x) # revealed: int | str
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: None
|
||||||
|
```
|
||||||
|
|||||||
@@ -89,3 +89,18 @@ def _(flag1: bool, flag2: bool, a: int):
|
|||||||
else:
|
else:
|
||||||
reveal_type(x) # revealed: Literal[1, 2]
|
reveal_type(x) # revealed: Literal[1, 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def f() -> Literal[1, 2, 3]:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if (x := f()) != 1:
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
else:
|
||||||
|
# TODO should be Literal[1]
|
||||||
|
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Narrowing for `match` statements
|
# Narrowing for `match` statements
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
## Single `match` pattern
|
## Single `match` pattern
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -34,8 +39,7 @@ match x:
|
|||||||
case A():
|
case A():
|
||||||
reveal_type(x) # revealed: A
|
reveal_type(x) # revealed: A
|
||||||
case B():
|
case B():
|
||||||
# TODO could be `B & ~A`
|
reveal_type(x) # revealed: B & ~A
|
||||||
reveal_type(x) # revealed: B
|
|
||||||
|
|
||||||
reveal_type(x) # revealed: object
|
reveal_type(x) # revealed: object
|
||||||
```
|
```
|
||||||
@@ -83,7 +87,7 @@ match x:
|
|||||||
case 6.0:
|
case 6.0:
|
||||||
reveal_type(x) # revealed: float
|
reveal_type(x) # revealed: float
|
||||||
case 1j:
|
case 1j:
|
||||||
reveal_type(x) # revealed: complex
|
reveal_type(x) # revealed: complex & ~float
|
||||||
case b"foo":
|
case b"foo":
|
||||||
reveal_type(x) # revealed: Literal[b"foo"]
|
reveal_type(x) # revealed: Literal[b"foo"]
|
||||||
|
|
||||||
@@ -129,11 +133,11 @@ match x:
|
|||||||
case "foo" | 42 | None:
|
case "foo" | 42 | None:
|
||||||
reveal_type(x) # revealed: Literal["foo", 42] | None
|
reveal_type(x) # revealed: Literal["foo", 42] | None
|
||||||
case "foo" | tuple():
|
case "foo" | tuple():
|
||||||
reveal_type(x) # revealed: Literal["foo"] | tuple
|
reveal_type(x) # revealed: tuple
|
||||||
case True | False:
|
case True | False:
|
||||||
reveal_type(x) # revealed: bool
|
reveal_type(x) # revealed: bool
|
||||||
case 3.14 | 2.718 | 1.414:
|
case 3.14 | 2.718 | 1.414:
|
||||||
reveal_type(x) # revealed: float
|
reveal_type(x) # revealed: float & ~tuple
|
||||||
|
|
||||||
reveal_type(x) # revealed: object
|
reveal_type(x) # revealed: object
|
||||||
```
|
```
|
||||||
@@ -160,3 +164,49 @@ match x:
|
|||||||
|
|
||||||
reveal_type(x) # revealed: object
|
reveal_type(x) # revealed: object
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Narrowing due to guard
|
||||||
|
|
||||||
|
```py
|
||||||
|
def get_object() -> object:
|
||||||
|
return object()
|
||||||
|
|
||||||
|
x = get_object()
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: object
|
||||||
|
|
||||||
|
match x:
|
||||||
|
case str() | float() if type(x) is str:
|
||||||
|
reveal_type(x) # revealed: str
|
||||||
|
case "foo" | 42 | None if isinstance(x, int):
|
||||||
|
reveal_type(x) # revealed: Literal[42]
|
||||||
|
case False if x:
|
||||||
|
reveal_type(x) # revealed: Never
|
||||||
|
case "foo" if x := "bar":
|
||||||
|
reveal_type(x) # revealed: Literal["bar"]
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: object
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guard and reveal_type in guard
|
||||||
|
|
||||||
|
```py
|
||||||
|
def get_object() -> object:
|
||||||
|
return object()
|
||||||
|
|
||||||
|
x = get_object()
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: object
|
||||||
|
|
||||||
|
match x:
|
||||||
|
case str() | float() if type(x) is str and reveal_type(x): # revealed: str
|
||||||
|
pass
|
||||||
|
case "foo" | 42 | None if isinstance(x, int) and reveal_type(x): # revealed: Literal[42]
|
||||||
|
pass
|
||||||
|
case False if x and reveal_type(x): # revealed: Never
|
||||||
|
pass
|
||||||
|
case "foo" if (x := "bar") and reveal_type(x): # revealed: Literal["bar"]
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(x) # revealed: object
|
||||||
|
```
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class MetaTruthy(type):
|
|||||||
|
|
||||||
class MetaDeferred(type):
|
class MetaDeferred(type):
|
||||||
def __bool__(self) -> MetaAmbiguous:
|
def __bool__(self) -> MetaAmbiguous:
|
||||||
return MetaAmbiguous()
|
raise NotImplementedError
|
||||||
|
|
||||||
class AmbiguousClass(metaclass=MetaAmbiguous): ...
|
class AmbiguousClass(metaclass=MetaAmbiguous): ...
|
||||||
class FalsyClass(metaclass=MetaFalsy): ...
|
class FalsyClass(metaclass=MetaFalsy): ...
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ def _(x: A | B):
|
|||||||
|
|
||||||
## Narrowing for generic classes
|
## Narrowing for generic classes
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.13"
|
||||||
|
```
|
||||||
|
|
||||||
Note that `type` returns the runtime class of an object, which does _not_ include specializations in
|
Note that `type` returns the runtime class of an object, which does _not_ include specializations in
|
||||||
the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the
|
the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the
|
||||||
specialization that we compare with; we must narrow to an unknown specialization of the generic
|
specialization that we compare with; we must narrow to an unknown specialization of the generic
|
||||||
@@ -139,3 +144,13 @@ def _(x: Base):
|
|||||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||||
reveal_type(x) # revealed: Base
|
reveal_type(x) # revealed: Base
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Assignment expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(x: object):
|
||||||
|
if (y := type(x)) is bool:
|
||||||
|
reveal_type(y) # revealed: Literal[bool]
|
||||||
|
if (type(y := x)) is bool:
|
||||||
|
reveal_type(y) # revealed: bool
|
||||||
|
```
|
||||||
|
|||||||
638
crates/red_knot_python_semantic/resources/mdtest/overloads.md
Normal file
638
crates/red_knot_python_semantic/resources/mdtest/overloads.md
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
# Overloads
|
||||||
|
|
||||||
|
Reference: <https://typing.python.org/en/latest/spec/overload.html>
|
||||||
|
|
||||||
|
## `typing.overload`
|
||||||
|
|
||||||
|
The definition of `typing.overload` in typeshed is an identity function.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
def foo(x: int) -> int:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(foo) # revealed: def foo(x: int) -> int
|
||||||
|
bar = overload(foo)
|
||||||
|
reveal_type(bar) # revealed: def foo(x: int) -> int
|
||||||
|
```
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def add() -> None: ...
|
||||||
|
@overload
|
||||||
|
def add(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def add(x: int, y: int) -> int: ...
|
||||||
|
def add(x: int | None = None, y: int | None = None) -> int | None:
|
||||||
|
return (x or 0) + (y or 0)
|
||||||
|
|
||||||
|
reveal_type(add) # revealed: Overload[() -> None, (x: int) -> int, (x: int, y: int) -> int]
|
||||||
|
reveal_type(add()) # revealed: None
|
||||||
|
reveal_type(add(1)) # revealed: int
|
||||||
|
reveal_type(add(1, 2)) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overriding
|
||||||
|
|
||||||
|
These scenarios are to verify that the overloaded and non-overloaded definitions are correctly
|
||||||
|
overridden by each other.
|
||||||
|
|
||||||
|
An overloaded function is overriding another overloaded function:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def foo() -> None: ...
|
||||||
|
@overload
|
||||||
|
def foo(x: int) -> int: ...
|
||||||
|
def foo(x: int | None = None) -> int | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(foo) # revealed: Overload[() -> None, (x: int) -> int]
|
||||||
|
reveal_type(foo()) # revealed: None
|
||||||
|
reveal_type(foo(1)) # revealed: int
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def foo() -> None: ...
|
||||||
|
@overload
|
||||||
|
def foo(x: str) -> str: ...
|
||||||
|
def foo(x: str | None = None) -> str | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(foo) # revealed: Overload[() -> None, (x: str) -> str]
|
||||||
|
reveal_type(foo()) # revealed: None
|
||||||
|
reveal_type(foo("")) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
A non-overloaded function is overriding an overloaded function:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def foo(x: int) -> int:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(foo) # revealed: def foo(x: int) -> int
|
||||||
|
```
|
||||||
|
|
||||||
|
An overloaded function is overriding a non-overloaded function:
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(foo) # revealed: def foo(x: int) -> int
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def foo() -> None: ...
|
||||||
|
@overload
|
||||||
|
def foo(x: bytes) -> bytes: ...
|
||||||
|
def foo(x: bytes | None = None) -> bytes | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(foo) # revealed: Overload[() -> None, (x: bytes) -> bytes]
|
||||||
|
reveal_type(foo()) # revealed: None
|
||||||
|
reveal_type(foo(b"")) # revealed: bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class Foo1:
|
||||||
|
@overload
|
||||||
|
def method(self) -> None: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
def method(self, x: int | None = None) -> int | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
foo1 = Foo1()
|
||||||
|
reveal_type(foo1.method) # revealed: Overload[() -> None, (x: int) -> int]
|
||||||
|
reveal_type(foo1.method()) # revealed: None
|
||||||
|
reveal_type(foo1.method(1)) # revealed: int
|
||||||
|
|
||||||
|
class Foo2:
|
||||||
|
@overload
|
||||||
|
def method(self) -> None: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
def method(self, x: str | None = None) -> str | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
foo2 = Foo2()
|
||||||
|
reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str]
|
||||||
|
reveal_type(foo2.method()) # revealed: None
|
||||||
|
reveal_type(foo2.method("")) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
@overload
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
@overload
|
||||||
|
def __init__(self, x: int) -> None: ...
|
||||||
|
def __init__(self, x: int | None = None) -> None:
|
||||||
|
self.x = x
|
||||||
|
|
||||||
|
foo = Foo()
|
||||||
|
reveal_type(foo) # revealed: Foo
|
||||||
|
reveal_type(foo.x) # revealed: Unknown | int | None
|
||||||
|
|
||||||
|
foo1 = Foo(1)
|
||||||
|
reveal_type(foo1) # revealed: Foo
|
||||||
|
reveal_type(foo1.x) # revealed: Unknown | int | None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version specific
|
||||||
|
|
||||||
|
Function definitions can vary between multiple Python versions.
|
||||||
|
|
||||||
|
### Overload and non-overload (3.9)
|
||||||
|
|
||||||
|
Here, the same function is overloaded in one version and not in another.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.9"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
import sys
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
def func(x: int) -> int:
|
||||||
|
return x
|
||||||
|
|
||||||
|
elif sys.version_info <= (3, 12):
|
||||||
|
@overload
|
||||||
|
def func() -> None: ...
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
def func(x: int | None = None) -> int | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(func) # revealed: def func(x: int) -> int
|
||||||
|
func() # error: [missing-argument]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overload and non-overload (3.10)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
import sys
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
def func(x: int) -> int:
|
||||||
|
return x
|
||||||
|
|
||||||
|
elif sys.version_info <= (3, 12):
|
||||||
|
@overload
|
||||||
|
def func() -> None: ...
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
def func(x: int | None = None) -> int | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int]
|
||||||
|
reveal_type(func()) # revealed: None
|
||||||
|
reveal_type(func(1)) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Some overloads are version specific (3.9)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.9"
|
||||||
|
```
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
import sys
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
@overload
|
||||||
|
def func() -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def func(x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`main.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from overloaded import func
|
||||||
|
|
||||||
|
reveal_type(func) # revealed: Overload[(x: int) -> int, (x: str) -> str]
|
||||||
|
func() # error: [no-matching-overload]
|
||||||
|
reveal_type(func(1)) # revealed: int
|
||||||
|
reveal_type(func("")) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
### Some overloads are version specific (3.10)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
import sys
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def func() -> None: ...
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def func(x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`main.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from overloaded import func
|
||||||
|
|
||||||
|
reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int, (x: str) -> str]
|
||||||
|
reveal_type(func()) # revealed: None
|
||||||
|
reveal_type(func(1)) # revealed: int
|
||||||
|
reveal_type(func("")) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
For an overloaded generic function, it's not necessary for all overloads to be generic.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def func() -> None: ...
|
||||||
|
@overload
|
||||||
|
def func[T](x: T) -> T: ...
|
||||||
|
def func[T](x: T | None = None) -> T | None:
|
||||||
|
return x
|
||||||
|
|
||||||
|
reveal_type(func) # revealed: Overload[() -> None, (x: T) -> T]
|
||||||
|
reveal_type(func()) # revealed: None
|
||||||
|
reveal_type(func(1)) # revealed: Literal[1]
|
||||||
|
reveal_type(func("")) # revealed: Literal[""]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invalid
|
||||||
|
|
||||||
|
### At least two overloads
|
||||||
|
|
||||||
|
At least two `@overload`-decorated definitions must be present.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
# TODO: error
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
def func(x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overload without an implementation
|
||||||
|
|
||||||
|
#### Regular modules
|
||||||
|
|
||||||
|
In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one
|
||||||
|
non-`@overload`-decorated definition (for the same function/method).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
# TODO: error because implementation does not exists
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def func(x: str) -> str: ...
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
# TODO: error because implementation does not exists
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stub files
|
||||||
|
|
||||||
|
Overload definitions within stub files are exempt from this check.
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def func(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def func(x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Protocols
|
||||||
|
|
||||||
|
Overload definitions within protocols are exempt from this check.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Protocol, overload
|
||||||
|
|
||||||
|
class Foo(Protocol):
|
||||||
|
@overload
|
||||||
|
def f(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def f(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Abstract methods
|
||||||
|
|
||||||
|
Overload definitions within abstract base classes are exempt from this check.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class AbstractFoo(ABC):
|
||||||
|
@overload
|
||||||
|
@abstractmethod
|
||||||
|
def f(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@abstractmethod
|
||||||
|
def f(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the `@abstractmethod` decorator requires that the class's metaclass is `ABCMeta` or is derived
|
||||||
|
from it.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
# TODO: Error because implementation does not exists
|
||||||
|
@overload
|
||||||
|
@abstractmethod
|
||||||
|
def f(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@abstractmethod
|
||||||
|
def f(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
And, the `@abstractmethod` decorator must be present on all the `@overload`-ed methods.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class PartialFoo1(ABC):
|
||||||
|
@overload
|
||||||
|
@abstractmethod
|
||||||
|
def f(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def f(self, x: str) -> str: ...
|
||||||
|
|
||||||
|
class PartialFoo(ABC):
|
||||||
|
@overload
|
||||||
|
def f(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@abstractmethod
|
||||||
|
def f(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inconsistent decorators
|
||||||
|
|
||||||
|
#### `@staticmethod` / `@classmethod`
|
||||||
|
|
||||||
|
If one overload signature is decorated with `@staticmethod` or `@classmethod`, all overload
|
||||||
|
signatures must be similarly decorated. The implementation, if present, must also have a consistent
|
||||||
|
decorator.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class CheckStaticMethod:
|
||||||
|
# TODO: error because `@staticmethod` does not exist on all overloads
|
||||||
|
@overload
|
||||||
|
def method1(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method1(x: str) -> str: ...
|
||||||
|
@staticmethod
|
||||||
|
def method1(x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
# TODO: error because `@staticmethod` does not exist on all overloads
|
||||||
|
@overload
|
||||||
|
def method2(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@staticmethod
|
||||||
|
def method2(x: str) -> str: ...
|
||||||
|
@staticmethod
|
||||||
|
def method2(x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
# TODO: error because `@staticmethod` does not exist on the implementation
|
||||||
|
@overload
|
||||||
|
@staticmethod
|
||||||
|
def method3(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@staticmethod
|
||||||
|
def method3(x: str) -> str: ...
|
||||||
|
def method3(x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@staticmethod
|
||||||
|
def method4(x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@staticmethod
|
||||||
|
def method4(x: str) -> str: ...
|
||||||
|
@staticmethod
|
||||||
|
def method4(x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
class CheckClassMethod:
|
||||||
|
def __init__(self, x: int) -> None:
|
||||||
|
self.x = x
|
||||||
|
# TODO: error because `@classmethod` does not exist on all overloads
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def try_from1(cls, x: int) -> CheckClassMethod: ...
|
||||||
|
@overload
|
||||||
|
def try_from1(cls, x: str) -> None: ...
|
||||||
|
@classmethod
|
||||||
|
def try_from1(cls, x: int | str) -> CheckClassMethod | None:
|
||||||
|
if isinstance(x, int):
|
||||||
|
return cls(x)
|
||||||
|
return None
|
||||||
|
# TODO: error because `@classmethod` does not exist on all overloads
|
||||||
|
@overload
|
||||||
|
def try_from2(cls, x: int) -> CheckClassMethod: ...
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def try_from2(cls, x: str) -> None: ...
|
||||||
|
@classmethod
|
||||||
|
def try_from2(cls, x: int | str) -> CheckClassMethod | None:
|
||||||
|
if isinstance(x, int):
|
||||||
|
return cls(x)
|
||||||
|
return None
|
||||||
|
# TODO: error because `@classmethod` does not exist on the implementation
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def try_from3(cls, x: int) -> CheckClassMethod: ...
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def try_from3(cls, x: str) -> None: ...
|
||||||
|
def try_from3(cls, x: int | str) -> CheckClassMethod | None:
|
||||||
|
if isinstance(x, int):
|
||||||
|
return cls(x)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def try_from4(cls, x: int) -> CheckClassMethod: ...
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def try_from4(cls, x: str) -> None: ...
|
||||||
|
@classmethod
|
||||||
|
def try_from4(cls, x: int | str) -> CheckClassMethod | None:
|
||||||
|
if isinstance(x, int):
|
||||||
|
return cls(x)
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `@final` / `@override`
|
||||||
|
|
||||||
|
If a `@final` or `@override` decorator is supplied for a function with overloads, the decorator
|
||||||
|
should be applied only to the overload implementation if it is present.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import final, overload, override
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
@overload
|
||||||
|
def method1(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method1(self, x: str) -> str: ...
|
||||||
|
@final
|
||||||
|
def method1(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
# TODO: error because `@final` is not on the implementation
|
||||||
|
@overload
|
||||||
|
@final
|
||||||
|
def method2(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method2(self, x: str) -> str: ...
|
||||||
|
def method2(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
# TODO: error because `@final` is not on the implementation
|
||||||
|
@overload
|
||||||
|
def method3(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@final
|
||||||
|
def method3(self, x: str) -> str: ...
|
||||||
|
def method3(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
def method(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
class Sub1(Base):
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
@override
|
||||||
|
def method(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
class Sub2(Base):
|
||||||
|
# TODO: error because `@override` is not on the implementation
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@override
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
def method(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
class Sub3(Base):
|
||||||
|
# TODO: error because `@override` is not on the implementation
|
||||||
|
@overload
|
||||||
|
@override
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
def method(self, x: int | str) -> int | str:
|
||||||
|
return x
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `@final` / `@override` in stub files
|
||||||
|
|
||||||
|
If an overload implementation isn’t present (for example, in a stub file), the `@final` or
|
||||||
|
`@override` decorator should be applied only to the first overload.
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing_extensions import final, overload, override
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
@overload
|
||||||
|
@final
|
||||||
|
def method1(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method1(self, x: str) -> str: ...
|
||||||
|
|
||||||
|
# TODO: error because `@final` is not on the first overload
|
||||||
|
@overload
|
||||||
|
def method2(self, x: int) -> int: ...
|
||||||
|
@final
|
||||||
|
@overload
|
||||||
|
def method2(self, x: str) -> str: ...
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
|
||||||
|
class Sub1(Base):
|
||||||
|
@overload
|
||||||
|
@override
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
|
||||||
|
class Sub2(Base):
|
||||||
|
# TODO: error because `@override` is not on the first overload
|
||||||
|
@overload
|
||||||
|
def method(self, x: int) -> int: ...
|
||||||
|
@overload
|
||||||
|
@override
|
||||||
|
def method(self, x: str) -> str: ...
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ reveal_type(__package__) # revealed: str | None
|
|||||||
reveal_type(__doc__) # revealed: str | None
|
reveal_type(__doc__) # revealed: str | None
|
||||||
reveal_type(__spec__) # revealed: ModuleSpec | None
|
reveal_type(__spec__) # revealed: ModuleSpec | None
|
||||||
|
|
||||||
reveal_type(__path__) # revealed: @Todo(generics)
|
reveal_type(__path__) # revealed: @Todo(specialized non-generic class)
|
||||||
|
|
||||||
class X:
|
class X:
|
||||||
reveal_type(__name__) # revealed: str
|
reveal_type(__name__) # revealed: str
|
||||||
@@ -59,7 +59,7 @@ reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: ob
|
|||||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||||
|
|
||||||
# TODO: needs support generics; should be `dict[str, Any]`:
|
# TODO: needs support generics; should be `dict[str, Any]`:
|
||||||
reveal_type(typing.__dict__) # revealed: @Todo(generics)
|
reveal_type(typing.__dict__) # revealed: @Todo(specialized non-generic class)
|
||||||
```
|
```
|
||||||
|
|
||||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||||
@@ -92,8 +92,8 @@ import foo
|
|||||||
from foo import __dict__ as foo_dict
|
from foo import __dict__ as foo_dict
|
||||||
|
|
||||||
# TODO: needs support generics; should be `dict[str, Any]` for both of these:
|
# TODO: needs support generics; should be `dict[str, Any]` for both of these:
|
||||||
reveal_type(foo.__dict__) # revealed: @Todo(generics)
|
reveal_type(foo.__dict__) # revealed: @Todo(specialized non-generic class)
|
||||||
reveal_type(foo_dict) # revealed: @Todo(generics)
|
reveal_type(foo_dict) # revealed: @Todo(specialized non-generic class)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conditionally global or `ModuleType` attribute
|
## Conditionally global or `ModuleType` attribute
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: version_related_syntax_errors.md - Version-related syntax error diagnostics - `match` statement - Before 3.10
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/version_related_syntax_errors.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
|
||||||
|
2 | case 1:
|
||||||
|
3 | print("it's one")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: invalid-syntax
|
||||||
|
--> /src/mdtest_snippet.py:1:1
|
||||||
|
|
|
||||||
|
1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
|
||||||
|
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||||
|
2 | case 1:
|
||||||
|
3 | print("it's one")
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
@@ -996,6 +996,11 @@ reveal_type(x) # revealed: Literal[1]
|
|||||||
|
|
||||||
## `match` statements
|
## `match` statements
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
### Single-valued types, always true
|
### Single-valued types, always true
|
||||||
|
|
||||||
```py
|
```py
|
||||||
@@ -1118,6 +1123,7 @@ def _(s: str):
|
|||||||
```toml
|
```toml
|
||||||
[environment]
|
[environment]
|
||||||
python-platform = "darwin"
|
python-platform = "darwin"
|
||||||
|
python-version = "3.10"
|
||||||
```
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
## Cyclical class definition
|
## Cyclical class definition
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ reveal_type(y) # revealed: Unknown
|
|||||||
|
|
||||||
def _(n: int):
|
def _(n: int):
|
||||||
a = b"abcde"[n]
|
a = b"abcde"[n]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
reveal_type(a) # revealed: int
|
||||||
reveal_type(a) # revealed: @Todo(return type of overloaded function)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Slices
|
## Slices
|
||||||
@@ -43,11 +42,9 @@ b[::0] # error: [zero-stepsize-in-slice]
|
|||||||
|
|
||||||
def _(m: int, n: int):
|
def _(m: int, n: int):
|
||||||
byte_slice1 = b[m:n]
|
byte_slice1 = b[m:n]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
reveal_type(byte_slice1) # revealed: bytes
|
||||||
reveal_type(byte_slice1) # revealed: @Todo(return type of overloaded function)
|
|
||||||
|
|
||||||
def _(s: bytes) -> bytes:
|
def _(s: bytes) -> bytes:
|
||||||
byte_slice2 = s[0:5]
|
byte_slice2 = s[0:5]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
return reveal_type(byte_slice2) # revealed: bytes
|
||||||
return reveal_type(byte_slice2) # revealed: @Todo(return type of overloaded function)
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ x = [1, 2, 3]
|
|||||||
reveal_type(x) # revealed: list
|
reveal_type(x) # revealed: list
|
||||||
|
|
||||||
# TODO reveal int
|
# TODO reveal int
|
||||||
reveal_type(x[0]) # revealed: @Todo(return type of overloaded function)
|
reveal_type(x[0]) # revealed: Unknown
|
||||||
|
|
||||||
# TODO reveal list
|
# TODO reveal list
|
||||||
reveal_type(x[0:1]) # revealed: @Todo(return type of overloaded function)
|
reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class)
|
||||||
|
|
||||||
# TODO error
|
# error: [call-non-callable]
|
||||||
reveal_type(x["a"]) # revealed: @Todo(return type of overloaded function)
|
reveal_type(x["a"]) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
## Assignments within list assignment
|
## Assignments within list assignment
|
||||||
@@ -29,9 +29,11 @@ In assignment, we might also have a named assignment. This should also get type
|
|||||||
x = [1, 2, 3]
|
x = [1, 2, 3]
|
||||||
x[0 if (y := 2) else 1] = 5
|
x[0 if (y := 2) else 1] = 5
|
||||||
|
|
||||||
# TODO error? (indeterminite index type)
|
# TODO: better error than "method `__getitem__` not callable on type `list`"
|
||||||
|
# error: [call-non-callable]
|
||||||
x["a" if (y := 2) else 1] = 6
|
x["a" if (y := 2) else 1] = 6
|
||||||
|
|
||||||
# TODO error (can't index via string)
|
# TODO: better error than "method `__getitem__` not callable on type `list`"
|
||||||
|
# error: [call-non-callable]
|
||||||
x["a" if (y := 2) else "b"] = 6
|
x["a" if (y := 2) else "b"] = 6
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ reveal_type(b) # revealed: Unknown
|
|||||||
|
|
||||||
def _(n: int):
|
def _(n: int):
|
||||||
a = "abcde"[n]
|
a = "abcde"[n]
|
||||||
# TODO: Support overloads... Should be `str`
|
reveal_type(a) # revealed: LiteralString
|
||||||
reveal_type(a) # revealed: @Todo(return type of overloaded function)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Slices
|
## Slices
|
||||||
@@ -75,12 +74,10 @@ def _(m: int, n: int, s2: str):
|
|||||||
s[::0] # error: [zero-stepsize-in-slice]
|
s[::0] # error: [zero-stepsize-in-slice]
|
||||||
|
|
||||||
substring1 = s[m:n]
|
substring1 = s[m:n]
|
||||||
# TODO: Support overloads... Should be `LiteralString`
|
reveal_type(substring1) # revealed: LiteralString
|
||||||
reveal_type(substring1) # revealed: @Todo(return type of overloaded function)
|
|
||||||
|
|
||||||
substring2 = s2[0:5]
|
substring2 = s2[0:5]
|
||||||
# TODO: Support overloads... Should be `str`
|
reveal_type(substring2) # revealed: str
|
||||||
reveal_type(substring2) # revealed: @Todo(return type of overloaded function)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Unsupported slice types
|
## Unsupported slice types
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ def _(m: int, n: int):
|
|||||||
t[::0] # error: [zero-stepsize-in-slice]
|
t[::0] # error: [zero-stepsize-in-slice]
|
||||||
|
|
||||||
tuple_slice = t[m:n]
|
tuple_slice = t[m:n]
|
||||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
# TODO: Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||||
reveal_type(tuple_slice) # revealed: @Todo(return type of overloaded function)
|
reveal_type(tuple_slice) # revealed: @Todo(full tuple[...] support)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inheritance
|
## Inheritance
|
||||||
@@ -117,6 +117,7 @@ from typing import Tuple
|
|||||||
|
|
||||||
class C(Tuple): ...
|
class C(Tuple): ...
|
||||||
|
|
||||||
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
# TODO: generic protocols
|
||||||
|
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||||
reveal_type(C.__mro__)
|
reveal_type(C.__mro__)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -23,16 +23,30 @@ def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
|
|||||||
reveal_type(n2) # revealed: int
|
reveal_type(n2) # revealed: int
|
||||||
reveal_type(n3) # revealed: ~int
|
reveal_type(n3) # revealed: ~int
|
||||||
|
|
||||||
def static_truthiness(not_one: Not[Literal[1]]) -> None:
|
|
||||||
static_assert(not_one != 1)
|
|
||||||
static_assert(not (not_one == 1))
|
|
||||||
|
|
||||||
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
|
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
|
||||||
n: Not[int, str]
|
n: Not[int, str]
|
||||||
|
|
||||||
|
def static_truthiness(not_one: Not[Literal[1]]) -> None:
|
||||||
|
# TODO: `bool` is not incorrect, but these would ideally be `Literal[True]` and `Literal[False]`
|
||||||
|
# respectively, since all possible runtime objects that are created by the literal syntax `1`
|
||||||
|
# are members of the type `Literal[1]`
|
||||||
|
reveal_type(not_one is not 1) # revealed: bool
|
||||||
|
reveal_type(not_one is 1) # revealed: bool
|
||||||
|
|
||||||
|
# But these are both `bool`, rather than `Literal[True]` or `Literal[False]`
|
||||||
|
# as there are many runtime objects that inhabit the type `~Literal[1]`
|
||||||
|
# but still compare equal to `1`. Two examples are `1.0` and `True`.
|
||||||
|
reveal_type(not_one != 1) # revealed: bool
|
||||||
|
reveal_type(not_one == 1) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
### Intersection
|
### Intersection
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||||
from typing_extensions import Literal, Never
|
from typing_extensions import Literal, Never
|
||||||
@@ -170,13 +184,11 @@ Static assertions can be used to enforce narrowing constraints:
|
|||||||
```py
|
```py
|
||||||
from knot_extensions import static_assert
|
from knot_extensions import static_assert
|
||||||
|
|
||||||
def f(x: int) -> None:
|
def f(x: int | None) -> None:
|
||||||
if x != 0:
|
if x is not None:
|
||||||
static_assert(x != 0)
|
static_assert(x is not None)
|
||||||
else:
|
else:
|
||||||
# `int` can be subclassed, so we cannot assert that `x == 0` here:
|
static_assert(x is None)
|
||||||
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
|
||||||
static_assert(x == 0)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Truthy expressions
|
### Truthy expressions
|
||||||
|
|||||||
@@ -504,4 +504,53 @@ c: Callable[[Any], str] = f
|
|||||||
c: Callable[[Any], str] = g
|
c: Callable[[Any], str] = g
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Method types
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def f(self, x: Any) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def g(self, x: Any) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
c: Callable[[Any], str] = A().f
|
||||||
|
|
||||||
|
# error: [invalid-assignment] "Object of type `bound method A.g(x: Any) -> int` is not assignable to `(Any, /) -> str`"
|
||||||
|
c: Callable[[Any], str] = A().g
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overloads
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import Any, overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overloaded() -> None: ...
|
||||||
|
@overload
|
||||||
|
def overloaded(a: str) -> str: ...
|
||||||
|
@overload
|
||||||
|
def overloaded(a: str, b: Any) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from overloaded import overloaded
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
c: Callable[[], None] = overloaded
|
||||||
|
c: Callable[[str], str] = overloaded
|
||||||
|
c: Callable[[str, Any], Any] = overloaded
|
||||||
|
c: Callable[..., str] = overloaded
|
||||||
|
|
||||||
|
# error: [invalid-assignment]
|
||||||
|
c: Callable[..., int] = overloaded
|
||||||
|
|
||||||
|
# error: [invalid-assignment]
|
||||||
|
c: Callable[[int], str] = overloaded
|
||||||
|
```
|
||||||
|
|
||||||
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||||
|
|||||||
@@ -246,6 +246,11 @@ static_assert(is_disjoint_from(Intersection[LiteralString, Not[AlwaysFalsy]], No
|
|||||||
|
|
||||||
### Class, module and function literals
|
### Class, module and function literals
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from types import ModuleType, FunctionType
|
from types import ModuleType, FunctionType
|
||||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||||
|
|||||||
@@ -254,4 +254,8 @@ from knot_extensions import is_equivalent_to, static_assert
|
|||||||
static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int))
|
static_assert(is_equivalent_to(int | Callable[[int | str], None], Callable[[str | int], None] | int))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Overloads
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
|
||||||
|
|||||||
@@ -99,3 +99,34 @@ static_assert(not is_fully_static(CallableTypeOf[f13]))
|
|||||||
static_assert(not is_fully_static(CallableTypeOf[f14]))
|
static_assert(not is_fully_static(CallableTypeOf[f14]))
|
||||||
static_assert(not is_fully_static(CallableTypeOf[f15]))
|
static_assert(not is_fully_static(CallableTypeOf[f15]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Overloads
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import Any, overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def gradual() -> None: ...
|
||||||
|
@overload
|
||||||
|
def gradual(a: Any) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def static() -> None: ...
|
||||||
|
@overload
|
||||||
|
def static(x: int) -> None: ...
|
||||||
|
@overload
|
||||||
|
def static(x: str) -> str: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import CallableTypeOf, TypeOf, is_fully_static, static_assert
|
||||||
|
from overloaded import gradual, static
|
||||||
|
|
||||||
|
static_assert(is_fully_static(TypeOf[gradual]))
|
||||||
|
static_assert(is_fully_static(TypeOf[static]))
|
||||||
|
|
||||||
|
static_assert(not is_fully_static(CallableTypeOf[gradual]))
|
||||||
|
static_assert(is_fully_static(CallableTypeOf[static]))
|
||||||
|
```
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]],
|
|||||||
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
|
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
|
||||||
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
|
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
|
||||||
|
|
||||||
# TODO: No errors
|
|
||||||
# error: [static-assert-error]
|
|
||||||
static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any))
|
static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any))
|
||||||
# error: [static-assert-error]
|
|
||||||
static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any]))
|
static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any]))
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -157,4 +154,6 @@ def f6(a, /): ...
|
|||||||
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
static_assert(not is_gradual_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f6]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
TODO: Overloads
|
||||||
|
|
||||||
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
[materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Subtype relation
|
# Subtype relation
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`.
|
The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`.
|
||||||
|
|
||||||
A fully static type `S` is a subtype of another fully static type `T` iff the set of values
|
A fully static type `S` is a subtype of another fully static type `T` iff the set of values
|
||||||
@@ -1148,5 +1153,187 @@ static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int]))
|
|||||||
static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int]))
|
static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Overloads
|
||||||
|
|
||||||
|
#### Subtype overloaded
|
||||||
|
|
||||||
|
For `B <: A`, if a callable `B` is overloaded with two or more signatures, it is a subtype of
|
||||||
|
callable `A` if _at least one_ of the overloaded signatures in `B` is a subtype of `A`.
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
class C: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overloaded(x: A) -> None: ...
|
||||||
|
@overload
|
||||||
|
def overloaded(x: B) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||||
|
from overloaded import A, B, C, overloaded
|
||||||
|
|
||||||
|
def accepts_a(x: A) -> None: ...
|
||||||
|
def accepts_b(x: B) -> None: ...
|
||||||
|
def accepts_c(x: C) -> None: ...
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_a]))
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_b]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[overloaded], CallableTypeOf[accepts_c]))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Supertype overloaded
|
||||||
|
|
||||||
|
For `B <: A`, if a callable `A` is overloaded with two or more signatures, callable `B` is a subtype
|
||||||
|
of `A` if `B` is a subtype of _all_ of the signatures in `A`.
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class Grandparent: ...
|
||||||
|
class Parent(Grandparent): ...
|
||||||
|
class Child(Parent): ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overloaded(a: Child) -> None: ...
|
||||||
|
@overload
|
||||||
|
def overloaded(a: Parent) -> None: ...
|
||||||
|
@overload
|
||||||
|
def overloaded(a: Grandparent) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||||
|
from overloaded import Grandparent, Parent, Child, overloaded
|
||||||
|
|
||||||
|
# This is a subtype of only the first overload
|
||||||
|
def child(a: Child) -> None: ...
|
||||||
|
|
||||||
|
# This is a subtype of the first and second overload
|
||||||
|
def parent(a: Parent) -> None: ...
|
||||||
|
|
||||||
|
# This is the only function that's a subtype of all overloads
|
||||||
|
def grandparent(a: Grandparent) -> None: ...
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[child], CallableTypeOf[overloaded]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[parent], CallableTypeOf[overloaded]))
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[grandparent], CallableTypeOf[overloaded]))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Both overloads
|
||||||
|
|
||||||
|
For `B <: A`, if both `A` and `B` is a callable that's overloaded with two or more signatures, then
|
||||||
|
`B` is a subtype of `A` if for _every_ signature in `A`, there is _at least one_ signature in `B`
|
||||||
|
that is a subtype of it.
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class Grandparent: ...
|
||||||
|
class Parent(Grandparent): ...
|
||||||
|
class Child(Parent): ...
|
||||||
|
class Other: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def pg(a: Parent) -> None: ...
|
||||||
|
@overload
|
||||||
|
def pg(a: Grandparent) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def po(a: Parent) -> None: ...
|
||||||
|
@overload
|
||||||
|
def po(a: Other) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def go(a: Grandparent) -> None: ...
|
||||||
|
@overload
|
||||||
|
def go(a: Other) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def cpg(a: Child) -> None: ...
|
||||||
|
@overload
|
||||||
|
def cpg(a: Parent) -> None: ...
|
||||||
|
@overload
|
||||||
|
def cpg(a: Grandparent) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def empty_go() -> Child: ...
|
||||||
|
@overload
|
||||||
|
def empty_go(a: Grandparent) -> None: ...
|
||||||
|
@overload
|
||||||
|
def empty_go(a: Other) -> Other: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def empty_cp() -> Parent: ...
|
||||||
|
@overload
|
||||||
|
def empty_cp(a: Child) -> None: ...
|
||||||
|
@overload
|
||||||
|
def empty_cp(a: Parent) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||||
|
from overloaded import pg, po, go, cpg, empty_go, empty_cp
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[pg], CallableTypeOf[cpg]))
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[cpg], CallableTypeOf[pg]))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[po], CallableTypeOf[pg]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[pg], CallableTypeOf[po]))
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[go], CallableTypeOf[pg]))
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[pg], CallableTypeOf[go]))
|
||||||
|
|
||||||
|
# Overload 1 in `empty_go` is a subtype of overload 1 in `empty_cp`
|
||||||
|
# Overload 2 in `empty_go` is a subtype of overload 2 in `empty_cp`
|
||||||
|
# Overload 2 in `empty_go` is a subtype of overload 3 in `empty_cp`
|
||||||
|
#
|
||||||
|
# All overloads in `empty_cp` has a subtype in `empty_go`
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[empty_go], CallableTypeOf[empty_cp]))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(CallableTypeOf[empty_cp], CallableTypeOf[empty_go]))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Order of overloads
|
||||||
|
|
||||||
|
Order of overloads is irrelevant for subtyping.
|
||||||
|
|
||||||
|
`overloaded.pyi`:
|
||||||
|
|
||||||
|
```pyi
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload_ab(x: A) -> None: ...
|
||||||
|
@overload
|
||||||
|
def overload_ab(x: B) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def overload_ba(x: B) -> None: ...
|
||||||
|
@overload
|
||||||
|
def overload_ba(x: A) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from overloaded import overload_ab, overload_ba
|
||||||
|
from knot_extensions import CallableTypeOf, is_subtype_of, static_assert
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[overload_ab], CallableTypeOf[overload_ba]))
|
||||||
|
static_assert(is_subtype_of(CallableTypeOf[overload_ba], CallableTypeOf[overload_ab]))
|
||||||
|
```
|
||||||
|
|
||||||
[special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
[special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||||
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
[typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||||
|
|||||||
@@ -166,3 +166,51 @@ def _(
|
|||||||
reveal_type(i1) # revealed: P & Q
|
reveal_type(i1) # revealed: P & Q
|
||||||
reveal_type(i2) # revealed: P & Q
|
reveal_type(i2) # revealed: P & Q
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Unions of literals with `AlwaysTruthy` and `AlwaysFalsy`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
from knot_extensions import AlwaysTruthy, AlwaysFalsy
|
||||||
|
|
||||||
|
type strings = Literal["foo", ""]
|
||||||
|
type ints = Literal[0, 1]
|
||||||
|
type bytes = Literal[b"foo", b""]
|
||||||
|
|
||||||
|
def _(
|
||||||
|
strings_or_truthy: strings | AlwaysTruthy,
|
||||||
|
truthy_or_strings: AlwaysTruthy | strings,
|
||||||
|
strings_or_falsy: strings | AlwaysFalsy,
|
||||||
|
falsy_or_strings: AlwaysFalsy | strings,
|
||||||
|
ints_or_truthy: ints | AlwaysTruthy,
|
||||||
|
truthy_or_ints: AlwaysTruthy | ints,
|
||||||
|
ints_or_falsy: ints | AlwaysFalsy,
|
||||||
|
falsy_or_ints: AlwaysFalsy | ints,
|
||||||
|
bytes_or_truthy: bytes | AlwaysTruthy,
|
||||||
|
truthy_or_bytes: AlwaysTruthy | bytes,
|
||||||
|
bytes_or_falsy: bytes | AlwaysFalsy,
|
||||||
|
falsy_or_bytes: AlwaysFalsy | bytes,
|
||||||
|
):
|
||||||
|
reveal_type(strings_or_truthy) # revealed: Literal[""] | AlwaysTruthy
|
||||||
|
reveal_type(truthy_or_strings) # revealed: AlwaysTruthy | Literal[""]
|
||||||
|
|
||||||
|
reveal_type(strings_or_falsy) # revealed: Literal["foo"] | AlwaysFalsy
|
||||||
|
reveal_type(falsy_or_strings) # revealed: AlwaysFalsy | Literal["foo"]
|
||||||
|
|
||||||
|
reveal_type(ints_or_truthy) # revealed: Literal[0] | AlwaysTruthy
|
||||||
|
reveal_type(truthy_or_ints) # revealed: AlwaysTruthy | Literal[0]
|
||||||
|
|
||||||
|
reveal_type(ints_or_falsy) # revealed: Literal[1] | AlwaysFalsy
|
||||||
|
reveal_type(falsy_or_ints) # revealed: AlwaysFalsy | Literal[1]
|
||||||
|
|
||||||
|
reveal_type(bytes_or_truthy) # revealed: Literal[b""] | AlwaysTruthy
|
||||||
|
reveal_type(truthy_or_bytes) # revealed: AlwaysTruthy | Literal[b""]
|
||||||
|
|
||||||
|
reveal_type(bytes_or_falsy) # revealed: Literal[b"foo"] | AlwaysFalsy
|
||||||
|
reveal_type(falsy_or_bytes) # revealed: AlwaysFalsy | Literal[b"foo"]
|
||||||
|
```
|
||||||
|
|||||||
@@ -708,3 +708,95 @@ with ContextManager() as (a, b, c):
|
|||||||
reveal_type(b) # revealed: Unknown
|
reveal_type(b) # revealed: Unknown
|
||||||
reveal_type(c) # revealed: Unknown
|
reveal_type(c) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Comprehension
|
||||||
|
|
||||||
|
Unpacking in a comprehension.
|
||||||
|
|
||||||
|
### Same types
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(arg: tuple[tuple[int, int], tuple[int, int]]):
|
||||||
|
# revealed: tuple[int, int]
|
||||||
|
[reveal_type((a, b)) for a, b in arg]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed types (1)
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(arg: tuple[tuple[int, int], tuple[int, str]]):
|
||||||
|
# revealed: tuple[int, int | str]
|
||||||
|
[reveal_type((a, b)) for a, b in arg]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed types (2)
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(arg: tuple[tuple[int, str], tuple[str, int]]):
|
||||||
|
# revealed: tuple[int | str, str | int]
|
||||||
|
[reveal_type((a, b)) for a, b in arg]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed types (3)
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]):
|
||||||
|
# revealed: tuple[int, int | str, int | bytes | str]
|
||||||
|
[reveal_type((a, b, c)) for a, b, c in arg]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Same literal values
|
||||||
|
|
||||||
|
```py
|
||||||
|
# revealed: tuple[Literal[1, 3], Literal[2, 4]]
|
||||||
|
[reveal_type((a, b)) for a, b in ((1, 2), (3, 4))]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed literal values (1)
|
||||||
|
|
||||||
|
```py
|
||||||
|
# revealed: tuple[Literal[1, "a"], Literal[2, "b"]]
|
||||||
|
[reveal_type((a, b)) for a, b in ((1, 2), ("a", "b"))]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed literals values (2)
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: "Object of type `Literal[1]` is not iterable"
|
||||||
|
# error: "Object of type `Literal[2]` is not iterable"
|
||||||
|
# error: "Object of type `Literal[4]` is not iterable"
|
||||||
|
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||||
|
# revealed: tuple[Unknown | Literal[3, 5], Unknown | Literal["a", "b"]]
|
||||||
|
[reveal_type((a, b)) for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c")]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom iterator (1)
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Iterator:
|
||||||
|
def __next__(self) -> tuple[int, int]:
|
||||||
|
return (1, 2)
|
||||||
|
|
||||||
|
class Iterable:
|
||||||
|
def __iter__(self) -> Iterator:
|
||||||
|
return Iterator()
|
||||||
|
|
||||||
|
# revealed: tuple[int, int]
|
||||||
|
[reveal_type((a, b)) for a, b in Iterable()]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom iterator (2)
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Iterator:
|
||||||
|
def __next__(self) -> bytes:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
class Iterable:
|
||||||
|
def __iter__(self) -> Iterator:
|
||||||
|
return Iterator()
|
||||||
|
|
||||||
|
def _(arg: tuple[tuple[int, str], Iterable]):
|
||||||
|
# revealed: tuple[int | bytes, str | bytes]
|
||||||
|
[reveal_type((a, b)) for a, b in arg]
|
||||||
|
```
|
||||||
|
|||||||
23
crates/red_knot_python_semantic/resources/primer/good.txt
Normal file
23
crates/red_knot_python_semantic/resources/primer/good.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
arrow
|
||||||
|
async-utils
|
||||||
|
bidict
|
||||||
|
black
|
||||||
|
dacite
|
||||||
|
git-revise
|
||||||
|
isort
|
||||||
|
itsdangerous
|
||||||
|
mypy_primer
|
||||||
|
packaging
|
||||||
|
paroxython
|
||||||
|
porcupine
|
||||||
|
psycopg
|
||||||
|
pybind11
|
||||||
|
pyinstrument
|
||||||
|
pyp
|
||||||
|
python-chess
|
||||||
|
python-htmlgen
|
||||||
|
rich
|
||||||
|
scrapy
|
||||||
|
typeshed-stats
|
||||||
|
werkzeug
|
||||||
|
zipp
|
||||||
@@ -98,6 +98,10 @@ pub(crate) mod tests {
|
|||||||
fn files(&self) -> &Files {
|
fn files(&self) -> &Files {
|
||||||
&self.files
|
&self.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn python_version(&self) -> PythonVersion {
|
||||||
|
Program::get(self).python_version(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Upcast<dyn SourceDb> for TestDb {
|
impl Upcast<dyn SourceDb> for TestDb {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ pub enum KnownModule {
|
|||||||
Sys,
|
Sys,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
Abc, // currently only used in tests
|
Abc, // currently only used in tests
|
||||||
|
Dataclasses,
|
||||||
Collections,
|
Collections,
|
||||||
Inspect,
|
Inspect,
|
||||||
KnotExtensions,
|
KnotExtensions,
|
||||||
@@ -132,6 +133,7 @@ impl KnownModule {
|
|||||||
Self::TypingExtensions => "typing_extensions",
|
Self::TypingExtensions => "typing_extensions",
|
||||||
Self::Sys => "sys",
|
Self::Sys => "sys",
|
||||||
Self::Abc => "abc",
|
Self::Abc => "abc",
|
||||||
|
Self::Dataclasses => "dataclasses",
|
||||||
Self::Collections => "collections",
|
Self::Collections => "collections",
|
||||||
Self::Inspect => "inspect",
|
Self::Inspect => "inspect",
|
||||||
Self::KnotExtensions => "knot_extensions",
|
Self::KnotExtensions => "knot_extensions",
|
||||||
|
|||||||
@@ -497,11 +497,10 @@ impl FusedIterator for ChildrenIter<'_> {}
|
|||||||
mod tests {
|
mod tests {
|
||||||
use ruff_db::files::{system_path_to_file, File};
|
use ruff_db::files::{system_path_to_file, File};
|
||||||
use ruff_db::parsed::parsed_module;
|
use ruff_db::parsed::parsed_module;
|
||||||
use ruff_db::system::DbWithWritableSystem as _;
|
use ruff_python_ast::{self as ast};
|
||||||
use ruff_python_ast as ast;
|
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::db::tests::TestDb;
|
use crate::db::tests::{TestDb, TestDbBuilder};
|
||||||
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
||||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||||
use crate::semantic_index::symbol::{
|
use crate::semantic_index::symbol::{
|
||||||
@@ -528,11 +527,15 @@ mod tests {
|
|||||||
file: File,
|
file: File,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_case(content: impl AsRef<str>) -> TestCase {
|
fn test_case(content: &str) -> TestCase {
|
||||||
let mut db = TestDb::new();
|
const FILENAME: &str = "test.py";
|
||||||
db.write_file("test.py", content).unwrap();
|
|
||||||
|
|
||||||
let file = system_path_to_file(&db, "test.py").unwrap();
|
let db = TestDbBuilder::new()
|
||||||
|
.with_file(FILENAME, content)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file = system_path_to_file(&db, FILENAME).unwrap();
|
||||||
|
|
||||||
TestCase { db, file }
|
TestCase { db, file }
|
||||||
}
|
}
|
||||||
@@ -937,7 +940,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
|||||||
panic!("expected generator definition")
|
panic!("expected generator definition")
|
||||||
};
|
};
|
||||||
let target = comprehension.target();
|
let target = comprehension.target();
|
||||||
let name = target.id().as_str();
|
let name = target.as_name_expr().unwrap().id().as_str();
|
||||||
|
|
||||||
assert_eq!(name, "x");
|
assert_eq!(name, "x");
|
||||||
assert_eq!(target.range(), TextRange::new(23.into(), 24.into()));
|
assert_eq!(target.range(), TextRange::new(23.into(), 24.into()));
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ pub trait HasScopedUseId {
|
|||||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId;
|
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasScopedUseId for ast::Identifier {
|
||||||
|
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||||
|
let ast_ids = ast_ids(db, scope);
|
||||||
|
ast_ids.use_id(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl HasScopedUseId for ast::ExprName {
|
impl HasScopedUseId for ast::ExprName {
|
||||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||||
let expression_ref = ExprRef::from(self);
|
let expression_ref = ExprRef::from(self);
|
||||||
@@ -157,7 +164,7 @@ impl AstIdsBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds `expr` to the use ids map and returns its id.
|
/// Adds `expr` to the use ids map and returns its id.
|
||||||
pub(super) fn record_use(&mut self, expr: &ast::Expr) -> ScopedUseId {
|
pub(super) fn record_use(&mut self, expr: impl Into<ExpressionNodeKey>) -> ScopedUseId {
|
||||||
let use_id = self.uses_map.len().into();
|
let use_id = self.uses_map.len().into();
|
||||||
|
|
||||||
self.uses_map.insert(expr.into(), use_id);
|
self.uses_map.insert(expr.into(), use_id);
|
||||||
@@ -196,4 +203,10 @@ pub(crate) mod node_key {
|
|||||||
Self(NodeKey::from_node(value))
|
Self(NodeKey::from_node(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&ast::Identifier> for ExpressionNodeKey {
|
||||||
|
fn from(value: &ast::Identifier) -> Self {
|
||||||
|
Self(NodeKey::from_node(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
|||||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||||
use crate::semantic_index::definition::{
|
use crate::semantic_index::definition::{
|
||||||
AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef,
|
AnnotatedAssignmentDefinitionKind, AnnotatedAssignmentDefinitionNodeRef,
|
||||||
AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef,
|
AssignmentDefinitionKind, AssignmentDefinitionNodeRef, ComprehensionDefinitionKind,
|
||||||
Definition, DefinitionCategory, DefinitionKind, DefinitionNodeKey, DefinitionNodeRef,
|
ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionKind,
|
||||||
Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionKind, ForStmtDefinitionNodeRef,
|
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef,
|
||||||
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
ForStmtDefinitionKind, ForStmtDefinitionNodeRef, ImportDefinitionNodeRef,
|
||||||
StarImportDefinitionNodeRef, TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef,
|
ImportFromDefinitionNodeRef, MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef,
|
||||||
|
TargetKind, WithItemDefinitionKind, WithItemDefinitionNodeRef,
|
||||||
};
|
};
|
||||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||||
use crate::semantic_index::predicate::{
|
use crate::semantic_index::predicate::{
|
||||||
@@ -354,15 +355,14 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
self.current_use_def_map_mut().merge(state);
|
self.current_use_def_map_mut().merge(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a 2-element tuple, where the first element is the [`ScopedSymbolId`] of the
|
/// Add a symbol to the symbol table and the use-def map.
|
||||||
/// symbol added, and the second element is a boolean indicating whether the symbol was *newly*
|
/// Return the [`ScopedSymbolId`] that uniquely identifies the symbol in both.
|
||||||
/// added or not
|
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
|
||||||
fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
|
|
||||||
let (symbol_id, added) = self.current_symbol_table().add_symbol(name);
|
let (symbol_id, added) = self.current_symbol_table().add_symbol(name);
|
||||||
if added {
|
if added {
|
||||||
self.current_use_def_map_mut().add_symbol(symbol_id);
|
self.current_use_def_map_mut().add_symbol(symbol_id);
|
||||||
}
|
}
|
||||||
(symbol_id, added)
|
symbol_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_attribute(&mut self, name: Name) -> ScopedSymbolId {
|
fn add_attribute(&mut self, name: Name) -> ScopedSymbolId {
|
||||||
@@ -569,7 +569,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Records a visibility constraint by applying it to all live bindings and declarations.
|
/// Records a visibility constraint by applying it to all live bindings and declarations.
|
||||||
#[must_use = "A visibility constraint must always be negated after it is added"]
|
|
||||||
fn record_visibility_constraint(
|
fn record_visibility_constraint(
|
||||||
&mut self,
|
&mut self,
|
||||||
predicate: Predicate<'db>,
|
predicate: Predicate<'db>,
|
||||||
@@ -797,7 +796,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
..
|
..
|
||||||
}) => (name, &None, default),
|
}) => (name, &None, default),
|
||||||
};
|
};
|
||||||
let (symbol, _) = self.add_symbol(name.id.clone());
|
let symbol = self.add_symbol(name.id.clone());
|
||||||
// TODO create Definition for PEP 695 typevars
|
// TODO create Definition for PEP 695 typevars
|
||||||
// note that the "bound" on the typevar is a totally different thing than whether
|
// note that the "bound" on the typevar is a totally different thing than whether
|
||||||
// or not a name is "bound" by a typevar declaration; the latter is always true.
|
// or not a name is "bound" by a typevar declaration; the latter is always true.
|
||||||
@@ -851,31 +850,35 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
|
|
||||||
// The `iter` of the first generator is evaluated in the outer scope, while all subsequent
|
// The `iter` of the first generator is evaluated in the outer scope, while all subsequent
|
||||||
// nodes are evaluated in the inner scope.
|
// nodes are evaluated in the inner scope.
|
||||||
self.add_standalone_expression(&generator.iter);
|
let value = self.add_standalone_expression(&generator.iter);
|
||||||
self.visit_expr(&generator.iter);
|
self.visit_expr(&generator.iter);
|
||||||
self.push_scope(scope);
|
self.push_scope(scope);
|
||||||
|
|
||||||
self.push_assignment(CurrentAssignment::Comprehension {
|
self.add_unpackable_assignment(
|
||||||
node: generator,
|
&Unpackable::Comprehension {
|
||||||
first: true,
|
node: generator,
|
||||||
});
|
first: true,
|
||||||
self.visit_expr(&generator.target);
|
},
|
||||||
self.pop_assignment();
|
&generator.target,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
for expr in &generator.ifs {
|
for expr in &generator.ifs {
|
||||||
self.visit_expr(expr);
|
self.visit_expr(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
for generator in generators_iter {
|
for generator in generators_iter {
|
||||||
self.add_standalone_expression(&generator.iter);
|
let value = self.add_standalone_expression(&generator.iter);
|
||||||
self.visit_expr(&generator.iter);
|
self.visit_expr(&generator.iter);
|
||||||
|
|
||||||
self.push_assignment(CurrentAssignment::Comprehension {
|
self.add_unpackable_assignment(
|
||||||
node: generator,
|
&Unpackable::Comprehension {
|
||||||
first: false,
|
node: generator,
|
||||||
});
|
first: false,
|
||||||
self.visit_expr(&generator.target);
|
},
|
||||||
self.pop_assignment();
|
&generator.target,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
for expr in &generator.ifs {
|
for expr in &generator.ifs {
|
||||||
self.visit_expr(expr);
|
self.visit_expr(expr);
|
||||||
@@ -891,20 +894,20 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
self.declare_parameter(parameter);
|
self.declare_parameter(parameter);
|
||||||
}
|
}
|
||||||
if let Some(vararg) = parameters.vararg.as_ref() {
|
if let Some(vararg) = parameters.vararg.as_ref() {
|
||||||
let (symbol, _) = self.add_symbol(vararg.name.id().clone());
|
let symbol = self.add_symbol(vararg.name.id().clone());
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
DefinitionNodeRef::VariadicPositionalParameter(vararg),
|
DefinitionNodeRef::VariadicPositionalParameter(vararg),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(kwarg) = parameters.kwarg.as_ref() {
|
if let Some(kwarg) = parameters.kwarg.as_ref() {
|
||||||
let (symbol, _) = self.add_symbol(kwarg.name.id().clone());
|
let symbol = self.add_symbol(kwarg.name.id().clone());
|
||||||
self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg));
|
self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
|
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
|
||||||
let (symbol, _) = self.add_symbol(parameter.name().id().clone());
|
let symbol = self.add_symbol(parameter.name().id().clone());
|
||||||
|
|
||||||
let definition = self.add_definition(symbol, parameter);
|
let definition = self.add_definition(symbol, parameter);
|
||||||
|
|
||||||
@@ -934,9 +937,30 @@ impl<'db> SemanticIndexBuilder<'db> {
|
|||||||
|
|
||||||
let current_assignment = match target {
|
let current_assignment = match target {
|
||||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||||
|
if matches!(unpackable, Unpackable::Comprehension { .. }) {
|
||||||
|
debug_assert_eq!(
|
||||||
|
self.scopes[self.current_scope()].node().scope_kind(),
|
||||||
|
ScopeKind::Comprehension
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The first iterator of the comprehension is evaluated in the outer scope, while all subsequent
|
||||||
|
// nodes are evaluated in the inner scope.
|
||||||
|
// SAFETY: The current scope is the comprehension, and the comprehension scope must have a parent scope.
|
||||||
|
let value_file_scope =
|
||||||
|
if let Unpackable::Comprehension { first: true, .. } = unpackable {
|
||||||
|
self.scope_stack
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.nth(1)
|
||||||
|
.expect("The comprehension scope must have a parent scope")
|
||||||
|
.file_scope_id
|
||||||
|
} else {
|
||||||
|
self.current_scope()
|
||||||
|
};
|
||||||
let unpack = Some(Unpack::new(
|
let unpack = Some(Unpack::new(
|
||||||
self.db,
|
self.db,
|
||||||
self.file,
|
self.file,
|
||||||
|
value_file_scope,
|
||||||
self.current_scope(),
|
self.current_scope(),
|
||||||
// SAFETY: `target` belongs to the `self.module` tree
|
// SAFETY: `target` belongs to the `self.module` tree
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
@@ -1114,7 +1138,18 @@ where
|
|||||||
// The symbol for the function name itself has to be evaluated
|
// The symbol for the function name itself has to be evaluated
|
||||||
// at the end to match the runtime evaluation of parameter defaults
|
// at the end to match the runtime evaluation of parameter defaults
|
||||||
// and return-type annotations.
|
// and return-type annotations.
|
||||||
let (symbol, _) = self.add_symbol(name.id.clone());
|
let symbol = self.add_symbol(name.id.clone());
|
||||||
|
|
||||||
|
// Record a use of the function name in the scope that it is defined in, so that it
|
||||||
|
// can be used to find previously defined functions with the same name. This is
|
||||||
|
// used to collect all the overloaded definitions of a function. This needs to be
|
||||||
|
// done on the `Identifier` node as opposed to `ExprName` because that's what the
|
||||||
|
// AST uses.
|
||||||
|
self.mark_symbol_used(symbol);
|
||||||
|
let use_id = self.current_ast_ids().record_use(name);
|
||||||
|
self.current_use_def_map_mut()
|
||||||
|
.record_use(symbol, use_id, NodeKey::from_node(name));
|
||||||
|
|
||||||
self.add_definition(symbol, function_def);
|
self.add_definition(symbol, function_def);
|
||||||
}
|
}
|
||||||
ast::Stmt::ClassDef(class) => {
|
ast::Stmt::ClassDef(class) => {
|
||||||
@@ -1138,11 +1173,11 @@ where
|
|||||||
);
|
);
|
||||||
|
|
||||||
// In Python runtime semantics, a class is registered after its scope is evaluated.
|
// In Python runtime semantics, a class is registered after its scope is evaluated.
|
||||||
let (symbol, _) = self.add_symbol(class.name.id.clone());
|
let symbol = self.add_symbol(class.name.id.clone());
|
||||||
self.add_definition(symbol, class);
|
self.add_definition(symbol, class);
|
||||||
}
|
}
|
||||||
ast::Stmt::TypeAlias(type_alias) => {
|
ast::Stmt::TypeAlias(type_alias) => {
|
||||||
let (symbol, _) = self.add_symbol(
|
let symbol = self.add_symbol(
|
||||||
type_alias
|
type_alias
|
||||||
.name
|
.name
|
||||||
.as_name_expr()
|
.as_name_expr()
|
||||||
@@ -1179,7 +1214,7 @@ where
|
|||||||
(Name::new(alias.name.id.split('.').next().unwrap()), false)
|
(Name::new(alias.name.id.split('.').next().unwrap()), false)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (symbol, _) = self.add_symbol(symbol_name);
|
let symbol = self.add_symbol(symbol_name);
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
ImportDefinitionNodeRef {
|
ImportDefinitionNodeRef {
|
||||||
@@ -1250,7 +1285,7 @@ where
|
|||||||
//
|
//
|
||||||
// For more details, see the doc-comment on `StarImportPlaceholderPredicate`.
|
// For more details, see the doc-comment on `StarImportPlaceholderPredicate`.
|
||||||
for export in exported_names(self.db, referenced_module) {
|
for export in exported_names(self.db, referenced_module) {
|
||||||
let (symbol_id, newly_added) = self.add_symbol(export.clone());
|
let symbol_id = self.add_symbol(export.clone());
|
||||||
let node_ref = StarImportDefinitionNodeRef { node, symbol_id };
|
let node_ref = StarImportDefinitionNodeRef { node, symbol_id };
|
||||||
let star_import = StarImportPlaceholderPredicate::new(
|
let star_import = StarImportPlaceholderPredicate::new(
|
||||||
self.db,
|
self.db,
|
||||||
@@ -1258,40 +1293,16 @@ where
|
|||||||
symbol_id,
|
symbol_id,
|
||||||
referenced_module,
|
referenced_module,
|
||||||
);
|
);
|
||||||
let pre_definition = self.flow_snapshot();
|
|
||||||
|
let pre_definition =
|
||||||
|
self.current_use_def_map().single_symbol_snapshot(symbol_id);
|
||||||
self.push_additional_definition(symbol_id, node_ref);
|
self.push_additional_definition(symbol_id, node_ref);
|
||||||
|
self.current_use_def_map_mut()
|
||||||
// Fast path for if there were no previous definitions
|
.record_and_negate_star_import_visibility_constraint(
|
||||||
// of the symbol defined through the `*` import:
|
star_import,
|
||||||
// we can apply the visibility constraint to *only* the added definition,
|
symbol_id,
|
||||||
// rather than all definitions
|
pre_definition,
|
||||||
if newly_added {
|
);
|
||||||
let constraint_id = self
|
|
||||||
.current_use_def_map_mut()
|
|
||||||
.record_star_import_visibility_constraint(
|
|
||||||
star_import,
|
|
||||||
symbol_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let post_definition = self.flow_snapshot();
|
|
||||||
self.flow_restore(pre_definition);
|
|
||||||
|
|
||||||
self.current_use_def_map_mut()
|
|
||||||
.negate_star_import_visibility_constraint(
|
|
||||||
symbol_id,
|
|
||||||
constraint_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.flow_merge(post_definition);
|
|
||||||
} else {
|
|
||||||
let constraint_id =
|
|
||||||
self.record_visibility_constraint(star_import.into());
|
|
||||||
let post_definition = self.flow_snapshot();
|
|
||||||
self.flow_restore(pre_definition.clone());
|
|
||||||
self.record_negated_visibility_constraint(constraint_id);
|
|
||||||
self.flow_merge(post_definition);
|
|
||||||
self.simplify_visibility_constraints(pre_definition);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -1311,7 +1322,7 @@ where
|
|||||||
self.has_future_annotations |= alias.name.id == "annotations"
|
self.has_future_annotations |= alias.name.id == "annotations"
|
||||||
&& node.module.as_deref() == Some("__future__");
|
&& node.module.as_deref() == Some("__future__");
|
||||||
|
|
||||||
let (symbol, _) = self.add_symbol(symbol_name.clone());
|
let symbol = self.add_symbol(symbol_name.clone());
|
||||||
|
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
@@ -1323,6 +1334,17 @@ where
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ast::Stmt::Assert(node) => {
|
||||||
|
self.visit_expr(&node.test);
|
||||||
|
let predicate = self.record_expression_narrowing_constraint(&node.test);
|
||||||
|
self.record_visibility_constraint(predicate);
|
||||||
|
|
||||||
|
if let Some(msg) = &node.msg {
|
||||||
|
self.visit_expr(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ast::Stmt::Assign(node) => {
|
ast::Stmt::Assign(node) => {
|
||||||
debug_assert_eq!(&self.current_assignments, &[]);
|
debug_assert_eq!(&self.current_assignments, &[]);
|
||||||
|
|
||||||
@@ -1583,54 +1605,76 @@ where
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let after_subject = self.flow_snapshot();
|
let mut no_case_matched = self.flow_snapshot();
|
||||||
let mut vis_constraints = vec![];
|
|
||||||
let mut post_case_snapshots = vec![];
|
|
||||||
for (i, case) in cases.iter().enumerate() {
|
|
||||||
if i != 0 {
|
|
||||||
post_case_snapshots.push(self.flow_snapshot());
|
|
||||||
self.flow_restore(after_subject.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let has_catchall = cases
|
||||||
|
.last()
|
||||||
|
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard());
|
||||||
|
|
||||||
|
let mut post_case_snapshots = vec![];
|
||||||
|
let mut match_predicate;
|
||||||
|
|
||||||
|
for (i, case) in cases.iter().enumerate() {
|
||||||
self.current_match_case = Some(CurrentMatchCase::new(&case.pattern));
|
self.current_match_case = Some(CurrentMatchCase::new(&case.pattern));
|
||||||
self.visit_pattern(&case.pattern);
|
self.visit_pattern(&case.pattern);
|
||||||
self.current_match_case = None;
|
self.current_match_case = None;
|
||||||
let predicate = self.add_pattern_narrowing_constraint(
|
// unlike in [Stmt::If], we don't reset [no_case_matched]
|
||||||
|
// here because the effects of visiting a pattern is binding
|
||||||
|
// symbols, and this doesn't occur unless the pattern
|
||||||
|
// actually matches
|
||||||
|
match_predicate = self.add_pattern_narrowing_constraint(
|
||||||
subject_expr,
|
subject_expr,
|
||||||
&case.pattern,
|
&case.pattern,
|
||||||
case.guard.as_deref(),
|
case.guard.as_deref(),
|
||||||
);
|
);
|
||||||
self.record_reachability_constraint(predicate);
|
let vis_constraint_id = self.record_reachability_constraint(match_predicate);
|
||||||
if let Some(expr) = &case.guard {
|
|
||||||
self.visit_expr(expr);
|
let match_success_guard_failure = case.guard.as_ref().map(|guard| {
|
||||||
}
|
let guard_expr = self.add_standalone_expression(guard);
|
||||||
|
self.visit_expr(guard);
|
||||||
|
let post_guard_eval = self.flow_snapshot();
|
||||||
|
let predicate = Predicate {
|
||||||
|
node: PredicateNode::Expression(guard_expr),
|
||||||
|
is_positive: true,
|
||||||
|
};
|
||||||
|
self.record_negated_narrowing_constraint(predicate);
|
||||||
|
let match_success_guard_failure = self.flow_snapshot();
|
||||||
|
self.flow_restore(post_guard_eval);
|
||||||
|
self.record_narrowing_constraint(predicate);
|
||||||
|
match_success_guard_failure
|
||||||
|
});
|
||||||
|
|
||||||
|
self.record_visibility_constraint_id(vis_constraint_id);
|
||||||
|
|
||||||
self.visit_body(&case.body);
|
self.visit_body(&case.body);
|
||||||
for id in &vis_constraints {
|
|
||||||
self.record_negated_visibility_constraint(*id);
|
|
||||||
}
|
|
||||||
let vis_constraint_id = self.record_visibility_constraint(predicate);
|
|
||||||
vis_constraints.push(vis_constraint_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no final wildcard match case, pretend there is one. This is similar to how
|
|
||||||
// we add an implicit `else` block in if-elif chains, in case it's not present.
|
|
||||||
if !cases
|
|
||||||
.last()
|
|
||||||
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
|
|
||||||
{
|
|
||||||
post_case_snapshots.push(self.flow_snapshot());
|
post_case_snapshots.push(self.flow_snapshot());
|
||||||
self.flow_restore(after_subject.clone());
|
|
||||||
|
|
||||||
for id in &vis_constraints {
|
if i != cases.len() - 1 || !has_catchall {
|
||||||
self.record_negated_visibility_constraint(*id);
|
// We need to restore the state after each case, but not after the last
|
||||||
|
// one. The last one will just become the state that we merge the other
|
||||||
|
// snapshots into.
|
||||||
|
self.flow_restore(no_case_matched.clone());
|
||||||
|
self.record_negated_narrowing_constraint(match_predicate);
|
||||||
|
if let Some(match_success_guard_failure) = match_success_guard_failure {
|
||||||
|
self.flow_merge(match_success_guard_failure);
|
||||||
|
} else {
|
||||||
|
assert!(case.guard.is_none());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug_assert!(match_success_guard_failure.is_none());
|
||||||
|
debug_assert!(case.guard.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.record_negated_visibility_constraint(vis_constraint_id);
|
||||||
|
no_case_matched = self.flow_snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
for post_clause_state in post_case_snapshots {
|
for post_clause_state in post_case_snapshots {
|
||||||
self.flow_merge(post_clause_state);
|
self.flow_merge(post_clause_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.simplify_visibility_constraints(after_subject);
|
self.simplify_visibility_constraints(no_case_matched);
|
||||||
}
|
}
|
||||||
ast::Stmt::Try(ast::StmtTry {
|
ast::Stmt::Try(ast::StmtTry {
|
||||||
body,
|
body,
|
||||||
@@ -1696,7 +1740,7 @@ where
|
|||||||
// which is invalid syntax. However, it's still pretty obvious here that the user
|
// which is invalid syntax. However, it's still pretty obvious here that the user
|
||||||
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
|
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
|
||||||
if let Some(symbol_name) = symbol_name {
|
if let Some(symbol_name) = symbol_name {
|
||||||
let (symbol, _) = self.add_symbol(symbol_name.id.clone());
|
let symbol = self.add_symbol(symbol_name.id.clone());
|
||||||
|
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
@@ -1772,7 +1816,7 @@ where
|
|||||||
let node_key = NodeKey::from_node(expr);
|
let node_key = NodeKey::from_node(expr);
|
||||||
|
|
||||||
match expr {
|
match expr {
|
||||||
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
|
ast::Expr::Name(ast::ExprName { id, ctx, .. }) => {
|
||||||
let (is_use, is_definition) = match (ctx, self.current_assignment()) {
|
let (is_use, is_definition) = match (ctx, self.current_assignment()) {
|
||||||
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
|
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
|
||||||
// For augmented assignment, the target expression is also used.
|
// For augmented assignment, the target expression is also used.
|
||||||
@@ -1783,7 +1827,7 @@ where
|
|||||||
(ast::ExprContext::Del, _) => (false, true),
|
(ast::ExprContext::Del, _) => (false, true),
|
||||||
(ast::ExprContext::Invalid, _) => (false, false),
|
(ast::ExprContext::Invalid, _) => (false, false),
|
||||||
};
|
};
|
||||||
let (symbol, _) = self.add_symbol(id.clone());
|
let symbol = self.add_symbol(id.clone());
|
||||||
|
|
||||||
if is_use {
|
if is_use {
|
||||||
self.mark_symbol_used(symbol);
|
self.mark_symbol_used(symbol);
|
||||||
@@ -1835,12 +1879,17 @@ where
|
|||||||
// implemented.
|
// implemented.
|
||||||
self.add_definition(symbol, named);
|
self.add_definition(symbol, named);
|
||||||
}
|
}
|
||||||
Some(CurrentAssignment::Comprehension { node, first }) => {
|
Some(CurrentAssignment::Comprehension {
|
||||||
|
unpack,
|
||||||
|
node,
|
||||||
|
first,
|
||||||
|
}) => {
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
ComprehensionDefinitionNodeRef {
|
ComprehensionDefinitionNodeRef {
|
||||||
|
unpack,
|
||||||
iterable: &node.iter,
|
iterable: &node.iter,
|
||||||
target: name_node,
|
target: expr,
|
||||||
first,
|
first,
|
||||||
is_async: node.is_async,
|
is_async: node.is_async,
|
||||||
},
|
},
|
||||||
@@ -2111,14 +2160,37 @@ where
|
|||||||
DefinitionKind::WithItem(assignment),
|
DefinitionKind::WithItem(assignment),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(CurrentAssignment::Comprehension { .. }) => {
|
Some(CurrentAssignment::Comprehension {
|
||||||
// TODO:
|
unpack,
|
||||||
|
node,
|
||||||
|
first,
|
||||||
|
}) => {
|
||||||
|
// SAFETY: `iter` and `expr` belong to the `self.module` tree
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
let assignment = ComprehensionDefinitionKind {
|
||||||
|
target_kind: TargetKind::from(unpack),
|
||||||
|
iterable: unsafe {
|
||||||
|
AstNodeRef::new(self.module.clone(), &node.iter)
|
||||||
|
},
|
||||||
|
target: unsafe { AstNodeRef::new(self.module.clone(), expr) },
|
||||||
|
first,
|
||||||
|
is_async: node.is_async,
|
||||||
|
};
|
||||||
|
// Temporarily move to the scope of the method to which the instance attribute is defined.
|
||||||
|
// SAFETY: `self.scope_stack` is not empty because the targets in comprehensions should always introduce a new scope.
|
||||||
|
let scope = self.scope_stack.pop().expect("The popped scope must be a comprehension, which must have a parent scope");
|
||||||
|
self.register_attribute_assignment(
|
||||||
|
object,
|
||||||
|
attr,
|
||||||
|
DefinitionKind::Comprehension(assignment),
|
||||||
|
);
|
||||||
|
self.scope_stack.push(scope);
|
||||||
}
|
}
|
||||||
Some(CurrentAssignment::AugAssign(_)) => {
|
Some(CurrentAssignment::AugAssign(_)) => {
|
||||||
// TODO:
|
// TODO:
|
||||||
}
|
}
|
||||||
Some(CurrentAssignment::Named(_)) => {
|
Some(CurrentAssignment::Named(_)) => {
|
||||||
// TODO:
|
// A named expression whose target is an attribute is syntactically prohibited
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
@@ -2159,7 +2231,7 @@ where
|
|||||||
range: _,
|
range: _,
|
||||||
}) = pattern
|
}) = pattern
|
||||||
{
|
{
|
||||||
let (symbol, _) = self.add_symbol(name.id().clone());
|
let symbol = self.add_symbol(name.id().clone());
|
||||||
let state = self.current_match_case.as_ref().unwrap();
|
let state = self.current_match_case.as_ref().unwrap();
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
@@ -2180,7 +2252,7 @@ where
|
|||||||
rest: Some(name), ..
|
rest: Some(name), ..
|
||||||
}) = pattern
|
}) = pattern
|
||||||
{
|
{
|
||||||
let (symbol, _) = self.add_symbol(name.id().clone());
|
let symbol = self.add_symbol(name.id().clone());
|
||||||
let state = self.current_match_case.as_ref().unwrap();
|
let state = self.current_match_case.as_ref().unwrap();
|
||||||
self.add_definition(
|
self.add_definition(
|
||||||
symbol,
|
symbol,
|
||||||
@@ -2212,6 +2284,7 @@ enum CurrentAssignment<'a> {
|
|||||||
Comprehension {
|
Comprehension {
|
||||||
node: &'a ast::Comprehension,
|
node: &'a ast::Comprehension,
|
||||||
first: bool,
|
first: bool,
|
||||||
|
unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||||
},
|
},
|
||||||
WithItem {
|
WithItem {
|
||||||
item: &'a ast::WithItem,
|
item: &'a ast::WithItem,
|
||||||
@@ -2225,11 +2298,9 @@ impl CurrentAssignment<'_> {
|
|||||||
match self {
|
match self {
|
||||||
Self::Assign { unpack, .. }
|
Self::Assign { unpack, .. }
|
||||||
| Self::For { unpack, .. }
|
| Self::For { unpack, .. }
|
||||||
| Self::WithItem { unpack, .. } => unpack.as_mut().map(|(position, _)| position),
|
| Self::WithItem { unpack, .. }
|
||||||
Self::AnnAssign(_)
|
| Self::Comprehension { unpack, .. } => unpack.as_mut().map(|(position, _)| position),
|
||||||
| Self::AugAssign(_)
|
Self::AnnAssign(_) | Self::AugAssign(_) | Self::Named(_) => None,
|
||||||
| Self::Named(_)
|
|
||||||
| Self::Comprehension { .. } => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2284,13 +2355,17 @@ enum Unpackable<'a> {
|
|||||||
item: &'a ast::WithItem,
|
item: &'a ast::WithItem,
|
||||||
is_async: bool,
|
is_async: bool,
|
||||||
},
|
},
|
||||||
|
Comprehension {
|
||||||
|
first: bool,
|
||||||
|
node: &'a ast::Comprehension,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Unpackable<'a> {
|
impl<'a> Unpackable<'a> {
|
||||||
const fn kind(&self) -> UnpackKind {
|
const fn kind(&self) -> UnpackKind {
|
||||||
match self {
|
match self {
|
||||||
Unpackable::Assign(_) => UnpackKind::Assign,
|
Unpackable::Assign(_) => UnpackKind::Assign,
|
||||||
Unpackable::For(_) => UnpackKind::Iterable,
|
Unpackable::For(_) | Unpackable::Comprehension { .. } => UnpackKind::Iterable,
|
||||||
Unpackable::WithItem { .. } => UnpackKind::ContextManager,
|
Unpackable::WithItem { .. } => UnpackKind::ContextManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2305,6 +2380,11 @@ impl<'a> Unpackable<'a> {
|
|||||||
is_async: *is_async,
|
is_async: *is_async,
|
||||||
unpack,
|
unpack,
|
||||||
},
|
},
|
||||||
|
Unpackable::Comprehension { node, first } => CurrentAssignment::Comprehension {
|
||||||
|
node,
|
||||||
|
first: *first,
|
||||||
|
unpack,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,8 +281,9 @@ pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
|
pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
|
||||||
|
pub(crate) unpack: Option<(UnpackPosition, Unpack<'a>)>,
|
||||||
pub(crate) iterable: &'a ast::Expr,
|
pub(crate) iterable: &'a ast::Expr,
|
||||||
pub(crate) target: &'a ast::ExprName,
|
pub(crate) target: &'a ast::Expr,
|
||||||
pub(crate) first: bool,
|
pub(crate) first: bool,
|
||||||
pub(crate) is_async: bool,
|
pub(crate) is_async: bool,
|
||||||
}
|
}
|
||||||
@@ -374,11 +375,13 @@ impl<'db> DefinitionNodeRef<'db> {
|
|||||||
is_async,
|
is_async,
|
||||||
}),
|
}),
|
||||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
||||||
|
unpack,
|
||||||
iterable,
|
iterable,
|
||||||
target,
|
target,
|
||||||
first,
|
first,
|
||||||
is_async,
|
is_async,
|
||||||
}) => DefinitionKind::Comprehension(ComprehensionDefinitionKind {
|
}) => DefinitionKind::Comprehension(ComprehensionDefinitionKind {
|
||||||
|
target_kind: TargetKind::from(unpack),
|
||||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||||
target: AstNodeRef::new(parsed, target),
|
target: AstNodeRef::new(parsed, target),
|
||||||
first,
|
first,
|
||||||
@@ -474,7 +477,9 @@ impl<'db> DefinitionNodeRef<'db> {
|
|||||||
unpack: _,
|
unpack: _,
|
||||||
is_async: _,
|
is_async: _,
|
||||||
}) => DefinitionNodeKey(NodeKey::from_node(target)),
|
}) => DefinitionNodeKey(NodeKey::from_node(target)),
|
||||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => {
|
||||||
|
DefinitionNodeKey(NodeKey::from_node(target))
|
||||||
|
}
|
||||||
Self::VariadicPositionalParameter(node) => node.into(),
|
Self::VariadicPositionalParameter(node) => node.into(),
|
||||||
Self::VariadicKeywordParameter(node) => node.into(),
|
Self::VariadicKeywordParameter(node) => node.into(),
|
||||||
Self::Parameter(node) => node.into(),
|
Self::Parameter(node) => node.into(),
|
||||||
@@ -550,7 +555,7 @@ pub enum DefinitionKind<'db> {
|
|||||||
AnnotatedAssignment(AnnotatedAssignmentDefinitionKind),
|
AnnotatedAssignment(AnnotatedAssignmentDefinitionKind),
|
||||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||||
For(ForStmtDefinitionKind<'db>),
|
For(ForStmtDefinitionKind<'db>),
|
||||||
Comprehension(ComprehensionDefinitionKind),
|
Comprehension(ComprehensionDefinitionKind<'db>),
|
||||||
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
|
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
|
||||||
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
|
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
|
||||||
Parameter(AstNodeRef<ast::ParameterWithDefault>),
|
Parameter(AstNodeRef<ast::ParameterWithDefault>),
|
||||||
@@ -749,19 +754,24 @@ impl MatchPatternDefinitionKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ComprehensionDefinitionKind {
|
pub struct ComprehensionDefinitionKind<'db> {
|
||||||
iterable: AstNodeRef<ast::Expr>,
|
pub(super) target_kind: TargetKind<'db>,
|
||||||
target: AstNodeRef<ast::ExprName>,
|
pub(super) iterable: AstNodeRef<ast::Expr>,
|
||||||
first: bool,
|
pub(super) target: AstNodeRef<ast::Expr>,
|
||||||
is_async: bool,
|
pub(super) first: bool,
|
||||||
|
pub(super) is_async: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComprehensionDefinitionKind {
|
impl<'db> ComprehensionDefinitionKind<'db> {
|
||||||
pub(crate) fn iterable(&self) -> &ast::Expr {
|
pub(crate) fn iterable(&self) -> &ast::Expr {
|
||||||
self.iterable.node()
|
self.iterable.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
pub(crate) fn target_kind(&self) -> TargetKind<'db> {
|
||||||
|
self.target_kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn target(&self) -> &ast::Expr {
|
||||||
self.target.node()
|
self.target.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,36 +26,37 @@ use ruff_python_ast::{
|
|||||||
name::Name,
|
name::Name,
|
||||||
visitor::{walk_expr, walk_pattern, walk_stmt, Visitor},
|
visitor::{walk_expr, walk_pattern, walk_stmt, Visitor},
|
||||||
};
|
};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{module_name::ModuleName, resolve_module, Db};
|
use crate::{module_name::ModuleName, resolve_module, Db};
|
||||||
|
|
||||||
fn exports_cycle_recover(
|
fn exports_cycle_recover(
|
||||||
_db: &dyn Db,
|
_db: &dyn Db,
|
||||||
_value: &FxHashSet<Name>,
|
_value: &[Name],
|
||||||
_count: u32,
|
_count: u32,
|
||||||
_file: File,
|
_file: File,
|
||||||
) -> salsa::CycleRecoveryAction<FxHashSet<Name>> {
|
) -> salsa::CycleRecoveryAction<Box<[Name]>> {
|
||||||
salsa::CycleRecoveryAction::Iterate
|
salsa::CycleRecoveryAction::Iterate
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exports_cycle_initial(_db: &dyn Db, _file: File) -> FxHashSet<Name> {
|
fn exports_cycle_initial(_db: &dyn Db, _file: File) -> Box<[Name]> {
|
||||||
FxHashSet::default()
|
Box::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)]
|
#[salsa::tracked(return_ref, cycle_fn=exports_cycle_recover, cycle_initial=exports_cycle_initial)]
|
||||||
pub(super) fn exported_names(db: &dyn Db, file: File) -> FxHashSet<Name> {
|
pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> {
|
||||||
let module = parsed_module(db.upcast(), file);
|
let module = parsed_module(db.upcast(), file);
|
||||||
let mut finder = ExportFinder::new(db, file);
|
let mut finder = ExportFinder::new(db, file);
|
||||||
finder.visit_body(module.suite());
|
finder.visit_body(module.suite());
|
||||||
finder.exports
|
finder.resolve_exports()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExportFinder<'db> {
|
struct ExportFinder<'db> {
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
file: File,
|
file: File,
|
||||||
visiting_stub_file: bool,
|
visiting_stub_file: bool,
|
||||||
exports: FxHashSet<Name>,
|
exports: FxHashMap<&'db Name, PossibleExportKind>,
|
||||||
|
dunder_all: DunderAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> ExportFinder<'db> {
|
impl<'db> ExportFinder<'db> {
|
||||||
@@ -64,30 +65,59 @@ impl<'db> ExportFinder<'db> {
|
|||||||
db,
|
db,
|
||||||
file,
|
file,
|
||||||
visiting_stub_file: file.is_stub(db.upcast()),
|
visiting_stub_file: file.is_stub(db.upcast()),
|
||||||
exports: FxHashSet::default(),
|
exports: FxHashMap::default(),
|
||||||
|
dunder_all: DunderAll::NotPresent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn possibly_add_export(&mut self, name: &Name) {
|
fn possibly_add_export(&mut self, export: &'db Name, kind: PossibleExportKind) {
|
||||||
if name.starts_with('_') {
|
self.exports.insert(export, kind);
|
||||||
return;
|
|
||||||
|
if export == "__all__" {
|
||||||
|
self.dunder_all = DunderAll::Present;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_exports(self) -> Box<[Name]> {
|
||||||
|
match self.dunder_all {
|
||||||
|
DunderAll::NotPresent => self
|
||||||
|
.exports
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(name, kind)| {
|
||||||
|
if kind == PossibleExportKind::StubImportWithoutRedundantAlias {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if name.starts_with('_') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(name.clone())
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
DunderAll::Present => self.exports.into_keys().cloned().collect(),
|
||||||
}
|
}
|
||||||
self.exports.insert(name.clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> Visitor<'db> for ExportFinder<'db> {
|
impl<'db> Visitor<'db> for ExportFinder<'db> {
|
||||||
fn visit_alias(&mut self, alias: &'db ast::Alias) {
|
fn visit_alias(&mut self, alias: &'db ast::Alias) {
|
||||||
let ast::Alias { name, asname, .. } = alias;
|
let ast::Alias {
|
||||||
if self.visiting_stub_file {
|
name,
|
||||||
// If the source is a stub, names defined by imports are only exported
|
asname,
|
||||||
// if they use the explicit `foo as foo` syntax:
|
range: _,
|
||||||
if asname.as_ref().is_some_and(|asname| asname.id == name.id) {
|
} = alias;
|
||||||
self.possibly_add_export(&name.id);
|
|
||||||
}
|
let name = &name.id;
|
||||||
|
let asname = asname.as_ref().map(|asname| &asname.id);
|
||||||
|
|
||||||
|
// If the source is a stub, names defined by imports are only exported
|
||||||
|
// if they use the explicit `foo as foo` syntax:
|
||||||
|
let kind = if self.visiting_stub_file && asname.is_none_or(|asname| asname != name) {
|
||||||
|
PossibleExportKind::StubImportWithoutRedundantAlias
|
||||||
} else {
|
} else {
|
||||||
self.possibly_add_export(&asname.as_ref().unwrap_or(name).id);
|
PossibleExportKind::Normal
|
||||||
}
|
};
|
||||||
|
|
||||||
|
self.possibly_add_export(asname.unwrap_or(name), kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_pattern(&mut self, pattern: &'db ast::Pattern) {
|
fn visit_pattern(&mut self, pattern: &'db ast::Pattern) {
|
||||||
@@ -106,7 +136,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
// all names with leading underscores, but this will not always be the case
|
// all names with leading underscores, but this will not always be the case
|
||||||
// (in the future we will want to support modules with `__all__ = ['_']`).
|
// (in the future we will want to support modules with `__all__ = ['_']`).
|
||||||
if name != "_" {
|
if name != "_" {
|
||||||
self.possibly_add_export(&name.id);
|
self.possibly_add_export(&name.id, PossibleExportKind::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,12 +150,12 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
self.visit_pattern(pattern);
|
self.visit_pattern(pattern);
|
||||||
}
|
}
|
||||||
if let Some(rest) = rest {
|
if let Some(rest) = rest {
|
||||||
self.possibly_add_export(&rest.id);
|
self.possibly_add_export(&rest.id, PossibleExportKind::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ast::Pattern::MatchStar(ast::PatternMatchStar { name, range: _ }) => {
|
ast::Pattern::MatchStar(ast::PatternMatchStar { name, range: _ }) => {
|
||||||
if let Some(name) = name {
|
if let Some(name) = name {
|
||||||
self.possibly_add_export(&name.id);
|
self.possibly_add_export(&name.id, PossibleExportKind::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ast::Pattern::MatchSequence(_)
|
ast::Pattern::MatchSequence(_)
|
||||||
@@ -137,7 +167,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_stmt(&mut self, stmt: &'db ruff_python_ast::Stmt) {
|
fn visit_stmt(&mut self, stmt: &'db ast::Stmt) {
|
||||||
match stmt {
|
match stmt {
|
||||||
ast::Stmt::ClassDef(ast::StmtClassDef {
|
ast::Stmt::ClassDef(ast::StmtClassDef {
|
||||||
name,
|
name,
|
||||||
@@ -147,7 +177,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
body: _, // We don't want to visit the body of the class
|
body: _, // We don't want to visit the body of the class
|
||||||
range: _,
|
range: _,
|
||||||
}) => {
|
}) => {
|
||||||
self.possibly_add_export(&name.id);
|
self.possibly_add_export(&name.id, PossibleExportKind::Normal);
|
||||||
for decorator in decorator_list {
|
for decorator in decorator_list {
|
||||||
self.visit_decorator(decorator);
|
self.visit_decorator(decorator);
|
||||||
}
|
}
|
||||||
@@ -155,6 +185,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
self.visit_arguments(arguments);
|
self.visit_arguments(arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
|
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||||
name,
|
name,
|
||||||
decorator_list,
|
decorator_list,
|
||||||
@@ -165,7 +196,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
range: _,
|
range: _,
|
||||||
is_async: _,
|
is_async: _,
|
||||||
}) => {
|
}) => {
|
||||||
self.possibly_add_export(&name.id);
|
self.possibly_add_export(&name.id, PossibleExportKind::Normal);
|
||||||
for decorator in decorator_list {
|
for decorator in decorator_list {
|
||||||
self.visit_decorator(decorator);
|
self.visit_decorator(decorator);
|
||||||
}
|
}
|
||||||
@@ -174,6 +205,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
self.visit_expr(returns);
|
self.visit_expr(returns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
|
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||||
target,
|
target,
|
||||||
value,
|
value,
|
||||||
@@ -189,6 +221,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
self.visit_expr(value);
|
self.visit_expr(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ast::Stmt::TypeAlias(ast::StmtTypeAlias {
|
ast::Stmt::TypeAlias(ast::StmtTypeAlias {
|
||||||
name,
|
name,
|
||||||
type_params: _,
|
type_params: _,
|
||||||
@@ -199,20 +232,22 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
// Neither walrus expressions nor statements cannot appear in type aliases;
|
// Neither walrus expressions nor statements cannot appear in type aliases;
|
||||||
// no need to recursively visit the `value` or `type_params`
|
// no need to recursively visit the `value` or `type_params`
|
||||||
}
|
}
|
||||||
|
|
||||||
ast::Stmt::ImportFrom(node) => {
|
ast::Stmt::ImportFrom(node) => {
|
||||||
let mut found_star = false;
|
let mut found_star = false;
|
||||||
for name in &node.names {
|
for name in &node.names {
|
||||||
if &name.name.id == "*" {
|
if &name.name.id == "*" {
|
||||||
if !found_star {
|
if !found_star {
|
||||||
found_star = true;
|
found_star = true;
|
||||||
self.exports.extend(
|
for export in
|
||||||
ModuleName::from_import_statement(self.db, self.file, node)
|
ModuleName::from_import_statement(self.db, self.file, node)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|module_name| resolve_module(self.db, &module_name))
|
.and_then(|module_name| resolve_module(self.db, &module_name))
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|module| exported_names(self.db, module.file()))
|
.flat_map(|module| exported_names(self.db, module.file()))
|
||||||
.cloned(),
|
{
|
||||||
);
|
self.possibly_add_export(export, PossibleExportKind::Normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.visit_alias(name);
|
self.visit_alias(name);
|
||||||
@@ -248,7 +283,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> {
|
|||||||
match expr {
|
match expr {
|
||||||
ast::Expr::Name(ast::ExprName { id, ctx, range: _ }) => {
|
ast::Expr::Name(ast::ExprName { id, ctx, range: _ }) => {
|
||||||
if ctx.is_store() {
|
if ctx.is_store() {
|
||||||
self.possibly_add_export(id);
|
self.possibly_add_export(id, PossibleExportKind::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +360,8 @@ impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> {
|
|||||||
range: _,
|
range: _,
|
||||||
}) = &**target
|
}) = &**target
|
||||||
{
|
{
|
||||||
self.export_finder.possibly_add_export(id);
|
self.export_finder
|
||||||
|
.possibly_add_export(id, PossibleExportKind::Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,3 +394,15 @@ impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum PossibleExportKind {
|
||||||
|
Normal,
|
||||||
|
StubImportWithoutRedundantAlias,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum DunderAll {
|
||||||
|
NotPresent,
|
||||||
|
Present,
|
||||||
|
}
|
||||||
|
|||||||
@@ -429,6 +429,14 @@ impl<'db> UseDefMap<'db> {
|
|||||||
self.declarations_iterator(declarations)
|
self.declarations_iterator(declarations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn all_public_declarations<'map>(
|
||||||
|
&'map self,
|
||||||
|
) -> impl Iterator<Item = (ScopedSymbolId, DeclarationsIterator<'map, 'db>)> + 'map {
|
||||||
|
(0..self.public_symbols.len())
|
||||||
|
.map(ScopedSymbolId::from_usize)
|
||||||
|
.map(|symbol_id| (symbol_id, self.public_declarations(symbol_id)))
|
||||||
|
}
|
||||||
|
|
||||||
/// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`.
|
/// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`.
|
||||||
pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool {
|
pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool {
|
||||||
!self
|
!self
|
||||||
@@ -551,6 +559,7 @@ impl<'db> Iterator for ConstraintsIterator<'_, 'db> {
|
|||||||
|
|
||||||
impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
|
impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct DeclarationsIterator<'map, 'db> {
|
pub(crate) struct DeclarationsIterator<'map, 'db> {
|
||||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||||
pub(crate) predicates: &'map Predicates<'db>,
|
pub(crate) predicates: &'map Predicates<'db>,
|
||||||
@@ -766,32 +775,76 @@ impl<'db> UseDefMapBuilder<'db> {
|
|||||||
.add_and_constraint(self.scope_start_visibility, constraint);
|
.add_and_constraint(self.scope_start_visibility, constraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use = "A `*`-import visibility constraint must always be negated after it is added"]
|
/// Snapshot the state of a single symbol at the current point in control flow.
|
||||||
pub(super) fn record_star_import_visibility_constraint(
|
///
|
||||||
|
/// This is only used for `*`-import visibility constraints, which are handled differently
|
||||||
|
/// to most other visibility constraints. See the doc-comment for
|
||||||
|
/// [`Self::record_and_negate_star_import_visibility_constraint`] for more details.
|
||||||
|
pub(super) fn single_symbol_snapshot(&self, symbol: ScopedSymbolId) -> SymbolState {
|
||||||
|
self.symbol_states[symbol].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method exists solely for handling `*`-import visibility constraints.
|
||||||
|
///
|
||||||
|
/// The reason why we add visibility constraints for [`Definition`]s created by `*` imports
|
||||||
|
/// is laid out in the doc-comment for [`StarImportPlaceholderPredicate`]. But treating these
|
||||||
|
/// visibility constraints in the use-def map the same way as all other visibility constraints
|
||||||
|
/// was shown to lead to [significant regressions] for small codebases where typeshed
|
||||||
|
/// dominates. (Although `*` imports are not common generally, they are used in several
|
||||||
|
/// important places by typeshed.)
|
||||||
|
///
|
||||||
|
/// To solve these regressions, it was observed that we could do significantly less work for
|
||||||
|
/// `*`-import definitions. We do a number of things differently here to our normal handling of
|
||||||
|
/// visibility constraints:
|
||||||
|
///
|
||||||
|
/// - We only apply and negate the visibility constraints to a single symbol, rather than to
|
||||||
|
/// all symbols. This is possible here because, unlike most definitions, we know in advance that
|
||||||
|
/// exactly one definition occurs inside the "if-true" predicate branch, and we know exactly
|
||||||
|
/// which definition it is.
|
||||||
|
///
|
||||||
|
/// Doing things this way is cheaper in and of itself. However, it also allows us to avoid
|
||||||
|
/// calling [`Self::simplify_visibility_constraints`] after the constraint has been applied to
|
||||||
|
/// the "if-predicate-true" branch and negated for the "if-predicate-false" branch. Simplifying
|
||||||
|
/// the visibility constraints is only important for symbols that did not have any new
|
||||||
|
/// definitions inside either the "if-predicate-true" branch or the "if-predicate-false" branch.
|
||||||
|
///
|
||||||
|
/// - We only snapshot the state for a single symbol prior to the definition, rather than doing
|
||||||
|
/// expensive calls to [`Self::snapshot`]. Again, this is possible because we know
|
||||||
|
/// that only a single definition occurs inside the "if-predicate-true" predicate branch.
|
||||||
|
///
|
||||||
|
/// - Normally we take care to check whether an "if-predicate-true" branch or an
|
||||||
|
/// "if-predicate-false" branch contains a terminal statement: these can affect the visibility
|
||||||
|
/// of symbols defined inside either branch. However, in the case of `*`-import definitions,
|
||||||
|
/// this is unnecessary (and therefore not done in this method), since we know that a `*`-import
|
||||||
|
/// predicate cannot create a terminal statement inside either branch.
|
||||||
|
///
|
||||||
|
/// [significant regressions]: https://github.com/astral-sh/ruff/pull/17286#issuecomment-2786755746
|
||||||
|
pub(super) fn record_and_negate_star_import_visibility_constraint(
|
||||||
&mut self,
|
&mut self,
|
||||||
star_import: StarImportPlaceholderPredicate<'db>,
|
star_import: StarImportPlaceholderPredicate<'db>,
|
||||||
symbol: ScopedSymbolId,
|
symbol: ScopedSymbolId,
|
||||||
) -> StarImportVisibilityConstraintId {
|
pre_definition_state: SymbolState,
|
||||||
|
) {
|
||||||
let predicate_id = self.add_predicate(star_import.into());
|
let predicate_id = self.add_predicate(star_import.into());
|
||||||
let visibility_id = self.visibility_constraints.add_atom(predicate_id);
|
let visibility_id = self.visibility_constraints.add_atom(predicate_id);
|
||||||
self.symbol_states[symbol]
|
let negated_visibility_id = self
|
||||||
.record_visibility_constraint(&mut self.visibility_constraints, visibility_id);
|
.visibility_constraints
|
||||||
StarImportVisibilityConstraintId(visibility_id)
|
.add_not_constraint(visibility_id);
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn negate_star_import_visibility_constraint(
|
let mut post_definition_state =
|
||||||
&mut self,
|
std::mem::replace(&mut self.symbol_states[symbol], pre_definition_state);
|
||||||
symbol_id: ScopedSymbolId,
|
|
||||||
constraint: StarImportVisibilityConstraintId,
|
post_definition_state
|
||||||
) {
|
.record_visibility_constraint(&mut self.visibility_constraints, visibility_id);
|
||||||
let negated_constraint = self
|
|
||||||
.visibility_constraints
|
self.symbol_states[symbol]
|
||||||
.add_not_constraint(constraint.into_scoped_constraint_id());
|
.record_visibility_constraint(&mut self.visibility_constraints, negated_visibility_id);
|
||||||
self.symbol_states[symbol_id]
|
|
||||||
.record_visibility_constraint(&mut self.visibility_constraints, negated_constraint);
|
self.symbol_states[symbol].merge(
|
||||||
self.scope_start_visibility = self
|
post_definition_state,
|
||||||
.visibility_constraints
|
&mut self.narrowing_constraints,
|
||||||
.add_and_constraint(self.scope_start_visibility, negated_constraint);
|
&mut self.visibility_constraints,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method resets the visibility constraints for all symbols to a previous state
|
/// This method resets the visibility constraints for all symbols to a previous state
|
||||||
@@ -1042,24 +1095,3 @@ impl<'db> UseDefMapBuilder<'db> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Newtype wrapper over [`ScopedVisibilityConstraintId`] to improve type safety.
|
|
||||||
///
|
|
||||||
/// By returning this type from [`UseDefMapBuilder::record_star_import_visibility_constraint`]
|
|
||||||
/// rather than [`ScopedVisibilityConstraintId`] directly, we ensure that
|
|
||||||
/// [`UseDefMapBuilder::negate_star_import_visibility_constraint`] must be called after the
|
|
||||||
/// visibility constraint has been added, and we ensure that
|
|
||||||
/// [`super::SemanticIndexBuilder::record_negated_visibility_constraint`] *cannot* be called with
|
|
||||||
/// the narrowing constraint (which would lead to incorrect behaviour).
|
|
||||||
///
|
|
||||||
/// This type is defined here rather than in the [`super::visibility_constraints`] module
|
|
||||||
/// because it should only ever be constructed and deconstructed from methods in the
|
|
||||||
/// [`UseDefMapBuilder`].
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub(super) struct StarImportVisibilityConstraintId(ScopedVisibilityConstraintId);
|
|
||||||
|
|
||||||
impl StarImportVisibilityConstraintId {
|
|
||||||
fn into_scoped_constraint_id(self) -> ScopedVisibilityConstraintId {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ impl SymbolBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub(super) struct SymbolState {
|
pub(in crate::semantic_index) struct SymbolState {
|
||||||
declarations: SymbolDeclarations,
|
declarations: SymbolDeclarations,
|
||||||
bindings: SymbolBindings,
|
bindings: SymbolBindings,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,11 @@
|
|||||||
//! flattens into the outer one), intersections cannot contain other intersections (also
|
//! flattens into the outer one), intersections cannot contain other intersections (also
|
||||||
//! flattens), and intersections cannot contain unions (the intersection distributes over the
|
//! flattens), and intersections cannot contain unions (the intersection distributes over the
|
||||||
//! union, inverting it into a union-of-intersections).
|
//! union, inverting it into a union-of-intersections).
|
||||||
|
//! * No type in a union can be a subtype of any other type in the union (just eliminate the
|
||||||
|
//! subtype from the union).
|
||||||
|
//! * 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 simplifies to [`Type::Never`].
|
||||||
//!
|
//!
|
||||||
//! The implication of these invariants is that a [`UnionBuilder`] does not necessarily build a
|
//! The implication of these invariants is that a [`UnionBuilder`] does not necessarily build a
|
||||||
//! [`Type::Union`]. For example, if only one type is added to the [`UnionBuilder`], `build()` will
|
//! [`Type::Union`]. For example, if only one type is added to the [`UnionBuilder`], `build()` will
|
||||||
@@ -19,19 +24,100 @@
|
|||||||
//! union type is added to the intersection, it will distribute and [`IntersectionBuilder::build`]
|
//! union type is added to the intersection, it will distribute and [`IntersectionBuilder::build`]
|
||||||
//! may end up returning a [`Type::Union`] of intersections.
|
//! may end up returning a [`Type::Union`] of intersections.
|
||||||
//!
|
//!
|
||||||
//! In the future we should have these additional invariants, but they aren't implemented yet:
|
//! ## Performance
|
||||||
//! * No type in a union can be a subtype of any other type in the union (just eliminate the
|
//!
|
||||||
//! subtype from the union).
|
//! In practice, there are two kinds of unions found in the wild: relatively-small unions made up
|
||||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
//! of normal user types (classes, etc), and large unions made up of literals, which can occur via
|
||||||
//! eliminate the supertype from the intersection).
|
//! large enums (not yet implemented) or from string/integer/bytes literals, which can grow due to
|
||||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
//! literal arithmetic or operations on literal strings/bytes. For normal unions, it's most
|
||||||
|
//! efficient to just store the member types in a vector, and do O(n^2) `is_subtype_of` checks to
|
||||||
|
//! maintain the union in simplified form. But literal unions can grow to a size where this becomes
|
||||||
|
//! a performance problem. For this reason, we group literal types in `UnionBuilder`. Since every
|
||||||
|
//! different string literal type shares exactly the same possible super-types, and none of them
|
||||||
|
//! are subtypes of each other (unless exactly the same literal type), we can avoid many
|
||||||
|
//! unnecessary `is_subtype_of` checks.
|
||||||
|
|
||||||
use crate::types::{IntersectionType, KnownClass, Type, TypeVarBoundOrConstraints, UnionType};
|
use crate::types::{
|
||||||
|
BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type,
|
||||||
|
TypeVarBoundOrConstraints, UnionType,
|
||||||
|
};
|
||||||
use crate::{Db, FxOrderSet};
|
use crate::{Db, FxOrderSet};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
enum UnionElement<'db> {
|
||||||
|
IntLiterals(FxOrderSet<i64>),
|
||||||
|
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
|
||||||
|
BytesLiterals(FxOrderSet<BytesLiteralType<'db>>),
|
||||||
|
Type(Type<'db>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> UnionElement<'db> {
|
||||||
|
/// Try reducing this `UnionElement` given the presence in the same union of `other_type`.
|
||||||
|
///
|
||||||
|
/// If this `UnionElement` is a group of literals, filter the literals present if needed and
|
||||||
|
/// return `ReduceResult::KeepIf` with a boolean value indicating whether the remaining group
|
||||||
|
/// of literals should be kept in the union
|
||||||
|
///
|
||||||
|
/// If this `UnionElement` is some other type, return `ReduceResult::Type` so `UnionBuilder`
|
||||||
|
/// can perform more complex checks on it.
|
||||||
|
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
|
||||||
|
// `AlwaysTruthy` and `AlwaysFalsy` are the only types which can be a supertype of only
|
||||||
|
// _some_ literals of the same kind, so we need to walk the full set in this case.
|
||||||
|
let needs_filter = matches!(other_type, Type::AlwaysTruthy | Type::AlwaysFalsy);
|
||||||
|
match self {
|
||||||
|
UnionElement::IntLiterals(literals) => {
|
||||||
|
ReduceResult::KeepIf(if needs_filter {
|
||||||
|
literals.retain(|literal| {
|
||||||
|
!Type::IntLiteral(*literal).is_subtype_of(db, other_type)
|
||||||
|
});
|
||||||
|
!literals.is_empty()
|
||||||
|
} else {
|
||||||
|
// SAFETY: All `UnionElement` literal kinds must always be non-empty
|
||||||
|
!Type::IntLiteral(literals[0]).is_subtype_of(db, other_type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
UnionElement::StringLiterals(literals) => {
|
||||||
|
ReduceResult::KeepIf(if needs_filter {
|
||||||
|
literals.retain(|literal| {
|
||||||
|
!Type::StringLiteral(*literal).is_subtype_of(db, other_type)
|
||||||
|
});
|
||||||
|
!literals.is_empty()
|
||||||
|
} else {
|
||||||
|
// SAFETY: All `UnionElement` literal kinds must always be non-empty
|
||||||
|
!Type::StringLiteral(literals[0]).is_subtype_of(db, other_type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
UnionElement::BytesLiterals(literals) => {
|
||||||
|
ReduceResult::KeepIf(if needs_filter {
|
||||||
|
literals.retain(|literal| {
|
||||||
|
!Type::BytesLiteral(*literal).is_subtype_of(db, other_type)
|
||||||
|
});
|
||||||
|
!literals.is_empty()
|
||||||
|
} else {
|
||||||
|
// SAFETY: All `UnionElement` literal kinds must always be non-empty
|
||||||
|
!Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
UnionElement::Type(existing) => ReduceResult::Type(*existing),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReduceResult<'db> {
|
||||||
|
/// Reduction of this `UnionElement` is complete; keep it in the union if the nested
|
||||||
|
/// boolean is true, eliminate it from the union if false.
|
||||||
|
KeepIf(bool),
|
||||||
|
/// The given `Type` can stand-in for the entire `UnionElement` for further union
|
||||||
|
/// simplification checks.
|
||||||
|
Type(Type<'db>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO increase this once we extend `UnionElement` throughout all union/intersection
|
||||||
|
// representations, so that we can make large unions of literals fast in all operations.
|
||||||
|
const MAX_UNION_LITERALS: usize = 200;
|
||||||
|
|
||||||
pub(crate) struct UnionBuilder<'db> {
|
pub(crate) struct UnionBuilder<'db> {
|
||||||
elements: Vec<Type<'db>>,
|
elements: Vec<UnionElement<'db>>,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,27 +134,118 @@ impl<'db> UnionBuilder<'db> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Collapse the union to a single type: `object`.
|
/// Collapse the union to a single type: `object`.
|
||||||
fn collapse_to_object(mut self) -> Self {
|
fn collapse_to_object(&mut self) {
|
||||||
self.elements.clear();
|
self.elements.clear();
|
||||||
self.elements.push(Type::object(self.db));
|
self.elements
|
||||||
self
|
.push(UnionElement::Type(Type::object(self.db)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a type to this union.
|
/// Adds a type to this union.
|
||||||
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
|
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
|
||||||
|
self.add_in_place(ty);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a type to this union.
|
||||||
|
pub(crate) fn add_in_place(&mut self, ty: Type<'db>) {
|
||||||
match ty {
|
match ty {
|
||||||
Type::Union(union) => {
|
Type::Union(union) => {
|
||||||
let new_elements = union.elements(self.db);
|
let new_elements = union.elements(self.db);
|
||||||
self.elements.reserve(new_elements.len());
|
self.elements.reserve(new_elements.len());
|
||||||
for element in new_elements {
|
for element in new_elements {
|
||||||
self = self.add(*element);
|
self.add_in_place(*element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Adding `Never` to a union is a no-op.
|
// Adding `Never` to a union is a no-op.
|
||||||
Type::Never => {}
|
Type::Never => {}
|
||||||
|
// If adding a string literal, look for an existing `UnionElement::StringLiterals` to
|
||||||
|
// add it to, or an existing element that is a super-type of string literals, which
|
||||||
|
// means we shouldn't add it. Otherwise, add a new `UnionElement::StringLiterals`
|
||||||
|
// containing it.
|
||||||
|
Type::StringLiteral(literal) => {
|
||||||
|
let mut found = false;
|
||||||
|
for element in &mut self.elements {
|
||||||
|
match element {
|
||||||
|
UnionElement::StringLiterals(literals) => {
|
||||||
|
if literals.len() >= MAX_UNION_LITERALS {
|
||||||
|
let replace_with = KnownClass::Str.to_instance(self.db);
|
||||||
|
self.add_in_place(replace_with);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
literals.insert(literal);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
self.elements
|
||||||
|
.push(UnionElement::StringLiterals(FxOrderSet::from_iter([
|
||||||
|
literal,
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Same for bytes literals as for string literals, above.
|
||||||
|
Type::BytesLiteral(literal) => {
|
||||||
|
let mut found = false;
|
||||||
|
for element in &mut self.elements {
|
||||||
|
match element {
|
||||||
|
UnionElement::BytesLiterals(literals) => {
|
||||||
|
if literals.len() >= MAX_UNION_LITERALS {
|
||||||
|
let replace_with = KnownClass::Bytes.to_instance(self.db);
|
||||||
|
self.add_in_place(replace_with);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
literals.insert(literal);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
self.elements
|
||||||
|
.push(UnionElement::BytesLiterals(FxOrderSet::from_iter([
|
||||||
|
literal,
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And same for int literals as well.
|
||||||
|
Type::IntLiteral(literal) => {
|
||||||
|
let mut found = false;
|
||||||
|
for element in &mut self.elements {
|
||||||
|
match element {
|
||||||
|
UnionElement::IntLiterals(literals) => {
|
||||||
|
if literals.len() >= MAX_UNION_LITERALS {
|
||||||
|
let replace_with = KnownClass::Int.to_instance(self.db);
|
||||||
|
self.add_in_place(replace_with);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
literals.insert(literal);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
UnionElement::Type(existing) if ty.is_subtype_of(self.db, *existing) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
self.elements
|
||||||
|
.push(UnionElement::IntLiterals(FxOrderSet::from_iter([literal])));
|
||||||
|
}
|
||||||
|
}
|
||||||
// Adding `object` to a union results in `object`.
|
// Adding `object` to a union results in `object`.
|
||||||
ty if ty.is_object(self.db) => {
|
ty if ty.is_object(self.db) => {
|
||||||
return self.collapse_to_object();
|
self.collapse_to_object();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let bool_pair = if let Type::BooleanLiteral(b) = ty {
|
let bool_pair = if let Type::BooleanLiteral(b) = ty {
|
||||||
@@ -81,8 +258,17 @@ impl<'db> UnionBuilder<'db> {
|
|||||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||||
let ty_negated = ty.negate(self.db);
|
let ty_negated = ty.negate(self.db);
|
||||||
|
|
||||||
for (index, element) in self.elements.iter().enumerate() {
|
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||||
if Some(*element) == bool_pair {
|
let element_type = match element.try_reduce(self.db, ty) {
|
||||||
|
ReduceResult::KeepIf(keep) => {
|
||||||
|
if !keep {
|
||||||
|
to_remove.push(index);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ReduceResult::Type(ty) => ty,
|
||||||
|
};
|
||||||
|
if Some(element_type) == bool_pair {
|
||||||
to_add = KnownClass::Bool.to_instance(self.db);
|
to_add = KnownClass::Bool.to_instance(self.db);
|
||||||
to_remove.push(index);
|
to_remove.push(index);
|
||||||
// The type we are adding is a BooleanLiteral, which doesn't have any
|
// The type we are adding is a BooleanLiteral, which doesn't have any
|
||||||
@@ -92,14 +278,14 @@ impl<'db> UnionBuilder<'db> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ty.is_same_gradual_form(*element)
|
if ty.is_gradual_equivalent_to(self.db, element_type)
|
||||||
|| ty.is_subtype_of(self.db, *element)
|
|| ty.is_subtype_of(self.db, element_type)
|
||||||
|| element.is_object(self.db)
|
|| element_type.is_object(self.db)
|
||||||
{
|
{
|
||||||
return self;
|
return;
|
||||||
} else if element.is_subtype_of(self.db, ty) {
|
} else if element_type.is_subtype_of(self.db, ty) {
|
||||||
to_remove.push(index);
|
to_remove.push(index);
|
||||||
} else if ty_negated.is_subtype_of(self.db, *element) {
|
} else if ty_negated.is_subtype_of(self.db, element_type) {
|
||||||
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
|
// We add `ty` to the union. We just checked that `~ty` is a subtype of an existing `element`.
|
||||||
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
|
// This also means that `~ty | ty` is a subtype of `element | ty`, because both elements in the
|
||||||
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
|
// first union are subtypes of the corresponding elements in the second union. But `~ty | ty` is
|
||||||
@@ -107,28 +293,43 @@ impl<'db> UnionBuilder<'db> {
|
|||||||
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
|
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
|
||||||
// the whole union to just `object`, since all other potential elements would also be subtypes of
|
// the whole union to just `object`, since all other potential elements would also be subtypes of
|
||||||
// `object`.
|
// `object`.
|
||||||
return self.collapse_to_object();
|
self.collapse_to_object();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((&first, rest)) = to_remove.split_first() {
|
if let Some((&first, rest)) = to_remove.split_first() {
|
||||||
self.elements[first] = to_add;
|
self.elements[first] = UnionElement::Type(to_add);
|
||||||
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
|
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
|
||||||
for &index in rest.iter().rev() {
|
for &index in rest.iter().rev() {
|
||||||
self.elements.swap_remove(index);
|
self.elements.swap_remove(index);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.elements.push(to_add);
|
self.elements.push(UnionElement::Type(to_add));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build(self) -> Type<'db> {
|
pub(crate) fn build(self) -> Type<'db> {
|
||||||
match self.elements.len() {
|
let mut types = vec![];
|
||||||
|
for element in self.elements {
|
||||||
|
match element {
|
||||||
|
UnionElement::IntLiterals(literals) => {
|
||||||
|
types.extend(literals.into_iter().map(Type::IntLiteral));
|
||||||
|
}
|
||||||
|
UnionElement::StringLiterals(literals) => {
|
||||||
|
types.extend(literals.into_iter().map(Type::StringLiteral));
|
||||||
|
}
|
||||||
|
UnionElement::BytesLiterals(literals) => {
|
||||||
|
types.extend(literals.into_iter().map(Type::BytesLiteral));
|
||||||
|
}
|
||||||
|
UnionElement::Type(ty) => types.push(ty),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match types.len() {
|
||||||
0 => Type::Never,
|
0 => Type::Never,
|
||||||
1 => self.elements[0],
|
1 => types[0],
|
||||||
_ => Type::Union(UnionType::new(self.db, self.elements.into_boxed_slice())),
|
_ => Type::Union(UnionType::new(self.db, types.into_boxed_slice())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +560,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||||
// S & T = S if S <: T
|
// S & T = S if S <: T
|
||||||
if existing_positive.is_subtype_of(db, new_positive)
|
if existing_positive.is_subtype_of(db, new_positive)
|
||||||
|| existing_positive.is_same_gradual_form(new_positive)
|
|| existing_positive.is_gradual_equivalent_to(db, new_positive)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -455,7 +656,9 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||||
// ~S & ~T = ~T if S <: T
|
// ~S & ~T = ~T if S <: T
|
||||||
if existing_negative.is_subtype_of(db, new_negative) {
|
if existing_negative.is_subtype_of(db, new_negative)
|
||||||
|
|| existing_negative.is_gradual_equivalent_to(db, new_negative)
|
||||||
|
{
|
||||||
to_remove.push(index);
|
to_remove.push(index);
|
||||||
}
|
}
|
||||||
// same rule, reverse order
|
// same rule, reverse order
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::Db;
|
|||||||
mod arguments;
|
mod arguments;
|
||||||
mod bind;
|
mod bind;
|
||||||
pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments};
|
pub(super) use arguments::{Argument, CallArgumentTypes, CallArguments};
|
||||||
pub(super) use bind::Bindings;
|
pub(super) use bind::{Bindings, CallableBinding};
|
||||||
|
|
||||||
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
|
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
|
||||||
/// unsuccessful.
|
/// unsuccessful.
|
||||||
|
|||||||
@@ -109,21 +109,6 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`CallArgumentTypes`] by prepending a synthetic argument to the front of this
|
|
||||||
/// argument list.
|
|
||||||
pub(crate) fn prepend_synthetic(&self, synthetic: Type<'db>) -> Self {
|
|
||||||
Self {
|
|
||||||
arguments: CallArguments(
|
|
||||||
std::iter::once(Argument::Synthetic)
|
|
||||||
.chain(self.arguments.iter())
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
types: std::iter::once(synthetic)
|
|
||||||
.chain(self.types.iter().copied())
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
|
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
|
||||||
self.arguments.iter().zip(self.types.iter().copied())
|
self.arguments.iter().zip(self.types.iter().copied())
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user