Compare commits
60 Commits
ibraheem/v
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87eec9bb51 | ||
|
|
eaed0d9b5c | ||
|
|
eb96456e1e | ||
|
|
3e0299488e | ||
|
|
7f0ce3e88d | ||
|
|
ba0736385d | ||
|
|
e41f045ec5 | ||
|
|
b24afb643c | ||
|
|
853bb00626 | ||
|
|
b5814b91c1 | ||
|
|
ea46426157 | ||
|
|
ddd2fc7a90 | ||
|
|
4ebf10cf1b | ||
|
|
9a676bbeb7 | ||
|
|
d9028a098b | ||
|
|
56077ee9a9 | ||
|
|
20c01d2553 | ||
|
|
c98ea1bc24 | ||
|
|
9a2990b2a1 | ||
|
|
a697050a83 | ||
|
|
2f64ef9c72 | ||
|
|
fde7d72fbb | ||
|
|
d13b5db066 | ||
|
|
c7b41060f4 | ||
|
|
3878701265 | ||
|
|
6e89e0abff | ||
|
|
cb31883c5f | ||
|
|
6d8f2864c3 | ||
|
|
990d0a8999 | ||
|
|
99beabdde8 | ||
|
|
3ae4db3ccd | ||
|
|
8ac5f9d8bc | ||
|
|
4abc5fe2f1 | ||
|
|
78ef241200 | ||
|
|
e4ba29392b | ||
|
|
29064034ba | ||
|
|
f1db842821 | ||
|
|
5a3deee353 | ||
|
|
e15f88ff21 | ||
|
|
a559275c3e | ||
|
|
95199c4217 | ||
|
|
f92a818fdd | ||
|
|
39ec97df79 | ||
|
|
a21988d820 | ||
|
|
eab41d5a4c | ||
|
|
52f4a529f7 | ||
|
|
8fd142f4ef | ||
|
|
5dca6d22df | ||
|
|
4c5846c6fe | ||
|
|
0289d1b163 | ||
|
|
09ff3e7056 | ||
|
|
7bacca9b62 | ||
|
|
2c68057c4b | ||
|
|
8e29be9c1c | ||
|
|
880513a013 | ||
|
|
046c5a46d8 | ||
|
|
cfed34334c | ||
|
|
11cc324449 | ||
|
|
c88e1a0663 | ||
|
|
2c7ac17b1e |
@@ -5,4 +5,4 @@ rustup component add clippy rustfmt
|
||||
cargo install cargo-insta
|
||||
cargo fetch
|
||||
|
||||
pip install maturin pre-commit
|
||||
pip install maturin prek
|
||||
|
||||
4
.github/actionlint.yaml
vendored
4
.github/actionlint.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
# Configuration for the actionlint tool, which we run via pre-commit
|
||||
# Configuration for the actionlint tool, which we run via prek
|
||||
# to verify the correctness of the syntax in our GitHub Actions workflows.
|
||||
|
||||
self-hosted-runner:
|
||||
@@ -17,4 +17,4 @@ self-hosted-runner:
|
||||
paths:
|
||||
".github/workflows/mypy_primer.yaml":
|
||||
ignore:
|
||||
- 'condition "false" is always evaluated to false. remove the if: section'
|
||||
- 'constant expression "false" in condition. remove the if: section'
|
||||
|
||||
1
.github/mypy-primer-ty.toml
vendored
1
.github/mypy-primer-ty.toml
vendored
@@ -6,3 +6,4 @@
|
||||
possibly-unresolved-reference = "warn"
|
||||
possibly-missing-import = "warn"
|
||||
division-by-zero = "warn"
|
||||
unsupported-dynamic-base = "warn"
|
||||
|
||||
4
.github/renovate.json5
vendored
4
.github/renovate.json5
vendored
@@ -76,9 +76,9 @@
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
groupName: "pre-commit dependencies",
|
||||
groupName: "prek dependencies",
|
||||
matchManagers: ["pre-commit"],
|
||||
description: "Weekly update of pre-commit dependencies",
|
||||
description: "Weekly update of prek dependencies",
|
||||
},
|
||||
{
|
||||
groupName: "NPM Development dependencies",
|
||||
|
||||
73
.github/workflows/ci.yaml
vendored
73
.github/workflows/ci.yaml
vendored
@@ -281,15 +281,15 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -343,11 +343,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -376,11 +376,11 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -486,7 +486,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
@@ -521,7 +521,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -561,7 +561,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
activate-environment: true
|
||||
@@ -667,7 +667,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -726,7 +726,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -769,32 +769,29 @@ jobs:
|
||||
- name: "Remove wheels from cache"
|
||||
run: rm -rf target/wheels
|
||||
|
||||
pre-commit:
|
||||
name: "pre-commit"
|
||||
prek:
|
||||
name: "prek"
|
||||
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
- name: "Cache pre-commit"
|
||||
- name: "Cache prek"
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: "Run pre-commit"
|
||||
path: ~/.cache/prek
|
||||
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: "Run prek"
|
||||
run: |
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
# Enable color output for prek and remove it for the summary
|
||||
# Use --hook-stage=manual to enable slower hooks that are skipped by default
|
||||
SKIP=rustfmt uvx prek run --all-files --show-diff-on-failure --color always --hook-stage manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -814,7 +811,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
activate-environment: true
|
||||
@@ -966,13 +963,13 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -980,7 +977,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@972e3437949c89e1357ebd1a2dbc852fcbc57245 # v4.5.1
|
||||
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
@@ -1011,7 +1008,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1044,10 +1041,10 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1061,7 +1058,7 @@ jobs:
|
||||
run: chmod +x target/codspeed/simulation/ruff_benchmark/ty
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@972e3437949c89e1357ebd1a2dbc852fcbc57245 # v4.5.1
|
||||
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run --bench ty "${{ matrix.benchmark }}"
|
||||
@@ -1092,13 +1089,13 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1133,10 +1130,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@28a9d316db64b78a951f3f8587a5d08cc97ad8eb # v2.65.6
|
||||
uses: taiki-e/install-action@0e76c5c569f13f7eb21e8e5b26fe710062b57b62 # v2.65.13
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1150,7 +1147,7 @@ jobs:
|
||||
run: chmod +x target/codspeed/walltime/ruff_benchmark/ty_walltime
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@972e3437949c89e1357ebd1a2dbc852fcbc57245 # v4.5.1
|
||||
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2
|
||||
env:
|
||||
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
|
||||
# appear to provide much useful insight for our walltime benchmarks right now
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
|
||||
6
.github/workflows/mypy_primer.yaml
vendored
6
.github/workflows/mypy_primer.yaml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
6
.github/workflows/sync_typeshed.yaml
vendored
6
.github/workflows/sync_typeshed.yaml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
- uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
6
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
6
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -38,14 +38,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
enable-cache: true
|
||||
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
lookup-only: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: rustup show
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
||||
@@ -21,15 +21,61 @@ exclude: |
|
||||
)$
|
||||
|
||||
repos:
|
||||
# Priority 0: Read-only hooks; hooks that modify disjoint file types.
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
priority: 0
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.24.1
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
priority: 0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.41.0
|
||||
hooks:
|
||||
- id: typos
|
||||
priority: 0
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rustfmt
|
||||
name: rustfmt
|
||||
entry: rustfmt
|
||||
language: system
|
||||
types: [rust]
|
||||
priority: 0
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.7.4
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
priority: 0
|
||||
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.19.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
priority: 0
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.36.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
priority: 0
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
priority: 0
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 1.0.0
|
||||
@@ -44,7 +90,20 @@ repos:
|
||||
docs/formatter/black\.md
|
||||
| docs/\w+\.md
|
||||
)$
|
||||
priority: 0
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
priority: 0
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
priority: 1
|
||||
|
||||
# Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat).
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.47.0
|
||||
hooks:
|
||||
@@ -54,7 +113,9 @@ repos:
|
||||
docs/formatter/black\.md
|
||||
| docs/\w+\.md
|
||||
)$
|
||||
priority: 1
|
||||
|
||||
# Priority 2: blacken-docs runs after markdownlint-fix (both modify markdown).
|
||||
- repo: https://github.com/adamchainz/blacken-docs
|
||||
rev: 1.20.0
|
||||
hooks:
|
||||
@@ -68,59 +129,18 @@ repos:
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==25.12.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.40.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
name: cargo fmt
|
||||
entry: cargo fmt --
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.7.4
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.19.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.36.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
priority: 2
|
||||
|
||||
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
|
||||
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.9
|
||||
rev: v1.7.10
|
||||
hooks:
|
||||
- id: actionlint
|
||||
stages:
|
||||
# This hook is disabled by default, since it's quite slow.
|
||||
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
|
||||
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
|
||||
# To run all hooks *including* this hook, use `uvx prek run -a --hook-stage=manual`.
|
||||
# To run *just* this hook, use `uvx prek run -a actionlint --hook-stage=manual`.
|
||||
- manual
|
||||
args:
|
||||
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
|
||||
@@ -131,8 +151,4 @@ repos:
|
||||
# and checks these with shellcheck. This is arguably its most useful feature,
|
||||
# but the integration only works if shellcheck is installed
|
||||
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.11.1"
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
priority: 0
|
||||
|
||||
@@ -65,6 +65,7 @@ When working on ty, PR titles should start with `[ty]` and be tagged with the `t
|
||||
- All changes must be tested. If you're not testing your changes, you're not done.
|
||||
- Get your tests to pass. If you didn't run the tests, your code does not work.
|
||||
- Follow existing code style. Check neighboring files for patterns.
|
||||
- Always run `uvx pre-commit run -a` at the end of a task.
|
||||
- Always run `uvx prek run -a` at the end of a task.
|
||||
- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.
|
||||
- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system.
|
||||
- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability.
|
||||
|
||||
@@ -53,12 +53,12 @@ cargo install cargo-insta
|
||||
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
|
||||
run Python utility commands.
|
||||
|
||||
You can optionally install pre-commit hooks to automatically run the validation checks
|
||||
You can optionally install hooks to automatically run the validation checks
|
||||
when making a commit:
|
||||
|
||||
```shell
|
||||
uv tool install pre-commit
|
||||
pre-commit install
|
||||
uv tool install prek
|
||||
prek install
|
||||
```
|
||||
|
||||
We recommend [nextest](https://nexte.st/) to run Ruff's test suite (via `cargo nextest run`),
|
||||
@@ -85,7 +85,7 @@ and that it passes both the lint and test validation checks:
|
||||
```shell
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
|
||||
RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
|
||||
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
|
||||
uvx prek run -a # Rust and Python formatting, Markdown and Python linting, etc.
|
||||
```
|
||||
|
||||
These checks will run on GitHub Actions when you open your pull request, but running them locally
|
||||
@@ -381,7 +381,7 @@ Commit each step of this process separately for easier review.
|
||||
|
||||
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
|
||||
- Changes should be edited to be user-facing descriptions, avoiding internal details
|
||||
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `pre-commit`
|
||||
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `prek`
|
||||
|
||||
Additionally, for minor releases:
|
||||
|
||||
|
||||
56
Cargo.lock
generated
56
Cargo.lock
generated
@@ -466,9 +466,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
version = "4.5.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -476,9 +476,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.53"
|
||||
version = "4.5.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1583,11 +1583,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "imperative"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29a1f6526af721f9aec9ceed7ab8ebfca47f3399d08b80056c2acca3fcb694a9"
|
||||
checksum = "35e1d0bd9c575c52e59aad8e122a11786e852a154678d0c86e9e243d55273970"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
"rust-stemmers",
|
||||
]
|
||||
|
||||
@@ -1648,9 +1648,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.45.1"
|
||||
version = "1.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c"
|
||||
checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5"
|
||||
dependencies = [
|
||||
"console 0.15.11",
|
||||
"once_cell",
|
||||
@@ -1874,9 +1874,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.178"
|
||||
version = "0.2.179"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -2488,7 +2488,17 @@ version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2498,7 +2508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_shared 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2507,7 +2517,7 @@ version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"phf_shared 0.11.3",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
@@ -2520,6 +2530,15 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -3286,7 +3305,6 @@ dependencies = [
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"is-macro",
|
||||
"itertools 0.14.0",
|
||||
"memchr",
|
||||
"ruff_cache",
|
||||
"ruff_macros",
|
||||
@@ -3994,9 +4012,9 @@ checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
version = "2.0.113"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4060,7 +4078,7 @@ checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"nom",
|
||||
"phf",
|
||||
"phf 0.11.3",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
@@ -4808,7 +4826,7 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"phf 0.11.3",
|
||||
"unicode_names2_generator",
|
||||
]
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
1657,
|
||||
1700,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -13,7 +13,10 @@ impl<T, C> AsFormat<C> for &T
|
||||
where
|
||||
T: AsFormat<C>,
|
||||
{
|
||||
type Format<'a> = T::Format<'a> where Self: 'a;
|
||||
type Format<'a>
|
||||
= T::Format<'a>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn format(&self) -> Self::Format<'_> {
|
||||
AsFormat::format(&**self)
|
||||
|
||||
@@ -106,6 +106,16 @@ impl Violation for PytestCompositeAssertion {
|
||||
/// assert exc_info.value.args
|
||||
/// ```
|
||||
///
|
||||
/// Or, for pytest 8.4.0 and later:
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// def test_foo():
|
||||
/// with pytest.raises(ZeroDivisionError, check=lambda e: e.args):
|
||||
/// 1 / 0
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some documented private methods, like `os._exit()`.
|
||||
// Allow some public functions whose names start with an underscore, like `os._exit()`.
|
||||
if let Some(qualified_name) = semantic.resolve_qualified_name(expr) {
|
||||
if matches!(qualified_name.segments(), ["os", "_exit"]) {
|
||||
return;
|
||||
|
||||
@@ -140,7 +140,7 @@ pub(crate) fn add_required_imports(
|
||||
source_type: PySourceType,
|
||||
context: &LintContext,
|
||||
) {
|
||||
for required_import in &settings.isort.required_imports {
|
||||
for required_import in settings.isort.required_imports.iter().rev() {
|
||||
add_required_import(
|
||||
required_import,
|
||||
parsed,
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
I002 [*] Missing required import: `from __future__ import annotations`
|
||||
--> docstring.py:1:1
|
||||
help: Insert required import: `from __future__ import annotations`
|
||||
1 | """Hello, world!"""
|
||||
2 + from __future__ import annotations
|
||||
3 |
|
||||
4 | x = 1
|
||||
|
||||
I002 [*] Missing required import: `from __future__ import generator_stop`
|
||||
--> docstring.py:1:1
|
||||
help: Insert required import: `from __future__ import generator_stop`
|
||||
@@ -16,3 +8,11 @@ help: Insert required import: `from __future__ import generator_stop`
|
||||
2 + from __future__ import generator_stop
|
||||
3 |
|
||||
4 | x = 1
|
||||
|
||||
I002 [*] Missing required import: `from __future__ import annotations`
|
||||
--> docstring.py:1:1
|
||||
help: Insert required import: `from __future__ import annotations`
|
||||
1 | """Hello, world!"""
|
||||
2 + from __future__ import annotations
|
||||
3 |
|
||||
4 | x = 1
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
I002 [*] Missing required import: `from __future__ import annotations`
|
||||
--> multiple_strings.py:1:1
|
||||
help: Insert required import: `from __future__ import annotations`
|
||||
1 | """This is a docstring."""
|
||||
2 + from __future__ import annotations
|
||||
3 | "This is not a docstring."
|
||||
4 | "This is also not a docstring."
|
||||
5 |
|
||||
|
||||
I002 [*] Missing required import: `from __future__ import generator_stop`
|
||||
--> multiple_strings.py:1:1
|
||||
help: Insert required import: `from __future__ import generator_stop`
|
||||
@@ -17,4 +8,13 @@ help: Insert required import: `from __future__ import generator_stop`
|
||||
2 + from __future__ import generator_stop
|
||||
3 | "This is not a docstring."
|
||||
4 | "This is also not a docstring."
|
||||
5 |
|
||||
|
||||
I002 [*] Missing required import: `from __future__ import annotations`
|
||||
--> multiple_strings.py:1:1
|
||||
help: Insert required import: `from __future__ import annotations`
|
||||
1 | """This is a docstring."""
|
||||
2 + from __future__ import annotations
|
||||
3 | "This is not a docstring."
|
||||
4 | "This is also not a docstring."
|
||||
5 |
|
||||
|
||||
@@ -398,17 +398,17 @@ mod tests {
|
||||
1 + from pipes import Template
|
||||
2 + from shlex import quote
|
||||
|
||||
I002 [*] Missing required import: `from __future__ import generator_stop`
|
||||
--> <filename>:1:1
|
||||
help: Insert required import: `from __future__ import generator_stop`
|
||||
1 + from __future__ import generator_stop
|
||||
2 | from pipes import quote, Template
|
||||
|
||||
I002 [*] Missing required import: `from collections import Sequence`
|
||||
--> <filename>:1:1
|
||||
help: Insert required import: `from collections import Sequence`
|
||||
1 + from collections import Sequence
|
||||
2 | from pipes import quote, Template
|
||||
|
||||
I002 [*] Missing required import: `from __future__ import generator_stop`
|
||||
--> <filename>:1:1
|
||||
help: Insert required import: `from __future__ import generator_stop`
|
||||
1 + from __future__ import generator_stop
|
||||
2 | from pipes import quote, Template
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::suppression::{InvalidSuppressionKind, ParseErrorKind};
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ruff: disable # missing codes
|
||||
/// # ruff: disable # missing codes
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
|
||||
@@ -28,7 +28,6 @@ bitflags = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
get-size2 = { workspace = true, optional = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true, optional = true }
|
||||
|
||||
@@ -14,7 +14,6 @@ use std::slice::{Iter, IterMut};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
@@ -3380,10 +3379,13 @@ impl Arguments {
|
||||
/// 2
|
||||
/// {'4': 5}
|
||||
/// ```
|
||||
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
|
||||
let args = self.args.iter().map(ArgOrKeyword::Arg);
|
||||
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
|
||||
args.merge_by(keywords, |left, right| left.start() <= right.start())
|
||||
pub fn arguments_source_order(&self) -> ArgumentsSourceOrder<'_> {
|
||||
ArgumentsSourceOrder {
|
||||
args: &self.args,
|
||||
keywords: &self.keywords,
|
||||
next_arg: 0,
|
||||
next_keyword: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_range(&self) -> TextRange {
|
||||
@@ -3399,6 +3401,38 @@ impl Arguments {
|
||||
}
|
||||
}
|
||||
|
||||
/// The iterator returned by [`Arguments::arguments_source_order`].
|
||||
#[derive(Clone)]
|
||||
pub struct ArgumentsSourceOrder<'a> {
|
||||
args: &'a [Expr],
|
||||
keywords: &'a [Keyword],
|
||||
next_arg: usize,
|
||||
next_keyword: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ArgumentsSourceOrder<'a> {
|
||||
type Item = ArgOrKeyword<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let arg = self.args.get(self.next_arg);
|
||||
let keyword = self.keywords.get(self.next_keyword);
|
||||
|
||||
if let Some(arg) = arg
|
||||
&& keyword.is_none_or(|keyword| arg.start() <= keyword.start())
|
||||
{
|
||||
self.next_arg += 1;
|
||||
Some(ArgOrKeyword::Arg(arg))
|
||||
} else if let Some(keyword) = keyword {
|
||||
self.next_keyword += 1;
|
||||
Some(ArgOrKeyword::Keyword(keyword))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for ArgumentsSourceOrder<'_> {}
|
||||
|
||||
/// An AST node used to represent a sequence of type parameters.
|
||||
///
|
||||
/// For example, given:
|
||||
|
||||
133
crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/semicolons.py
vendored
Normal file
133
crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/semicolons.py
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
class Simple:
|
||||
x=1
|
||||
x=2 # fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class TrailingSemicolon:
|
||||
x=1
|
||||
x=2;x=3 ; # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
x=1;
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
def foo(): y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
class MultiLineSemicolon:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
]; x=2 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAfter:
|
||||
x=1
|
||||
x = ['a']\
|
||||
; y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonBefore:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewline:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewlineAndComment:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
# 1
|
||||
y=1 # fmt: skip
|
||||
|
||||
class RepeatedLineContinuation:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
\
|
||||
\
|
||||
y=1 # fmt: skip
|
||||
|
||||
class MultiLineSemicolonComments:
|
||||
x=1
|
||||
# 1
|
||||
x = [ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
# 4
|
||||
]; x=2 # 5 # fmt: skip # 6
|
||||
|
||||
class DocstringSkipped:
|
||||
'''This is a docstring''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class MultilineDocstringSkipped:
|
||||
'''This is a docstring
|
||||
''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class FirstStatementNewlines:
|
||||
|
||||
|
||||
|
||||
|
||||
x=1 # fmt: skip
|
||||
|
||||
class ChainingSemicolons:
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
];x=1;x=[
|
||||
'1',
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # fmt: skip
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
class MixingCompound:
|
||||
def foo(): bar(); import zoo # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17331
|
||||
def main() -> None:
|
||||
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11430
|
||||
print(); print() # noqa # fmt: skip
|
||||
22
crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/top_level_semicolon.py
vendored
Normal file
22
crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/top_level_semicolon.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# 0
|
||||
'''Module docstring
|
||||
multiple lines''' # 1 # fmt: skip # 2
|
||||
|
||||
import a;import b; from c import (
|
||||
x,y,z
|
||||
); import f # fmt: skip
|
||||
|
||||
x=1;x=2;x=3;x=4 # fmt: skip
|
||||
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
def foo(): x=[
|
||||
'1',
|
||||
'2',
|
||||
];x=1 # fmt: skip
|
||||
68
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/fmt_skip.py
vendored
Normal file
68
crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/fmt_skip.py
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
class Simple:
|
||||
# Range comprises skip range
|
||||
x=1
|
||||
<RANGE_START>x=2 <RANGE_END># fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
# Range is part of skip range
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatFirst:
|
||||
x=1
|
||||
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatMiddle:
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on right side
|
||||
<RANGE_START>x=1;
|
||||
x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on left side
|
||||
x=1;
|
||||
x=2;<RANGE_START>x=3 # fmt: skip
|
||||
x=4<RANGE_END>
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
# Range inside statement
|
||||
x=1
|
||||
x = <RANGE_START>[
|
||||
'1',
|
||||
'2',<RANGE_END>
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3<RANGE_START>
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
@@ -24,7 +24,6 @@ pub use crate::options::{
|
||||
};
|
||||
use crate::range::is_logical_line;
|
||||
pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat};
|
||||
use crate::verbatim::suppressed_node;
|
||||
|
||||
pub(crate) mod builders;
|
||||
pub mod cli;
|
||||
@@ -61,51 +60,39 @@ where
|
||||
let node_ref = AnyNodeRef::from(node);
|
||||
let node_comments = comments.leading_dangling_trailing(node_ref);
|
||||
|
||||
if self.is_suppressed(node_comments.trailing, f.context()) {
|
||||
suppressed_node(node_ref).fmt(f)
|
||||
} else {
|
||||
leading_comments(node_comments.leading).fmt(f)?;
|
||||
leading_comments(node_comments.leading).fmt(f)?;
|
||||
|
||||
// Emit source map information for nodes that are valid "narrowing" targets
|
||||
// in range formatting. Never emit source map information if they're disabled
|
||||
// for performance reasons.
|
||||
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
|
||||
&& f.options().source_map_generation().is_enabled();
|
||||
// Emit source map information for nodes that are valid "narrowing" targets
|
||||
// in range formatting. Never emit source map information if they're disabled
|
||||
// for performance reasons.
|
||||
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
|
||||
&& f.options().source_map_generation().is_enabled();
|
||||
|
||||
emit_source_position
|
||||
.then_some(source_position(node.start()))
|
||||
.fmt(f)?;
|
||||
emit_source_position
|
||||
.then_some(source_position(node.start()))
|
||||
.fmt(f)?;
|
||||
|
||||
self.fmt_fields(node, f)?;
|
||||
self.fmt_fields(node, f)?;
|
||||
|
||||
debug_assert!(
|
||||
node_comments
|
||||
.dangling
|
||||
.iter()
|
||||
.all(SourceComment::is_formatted),
|
||||
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
|
||||
);
|
||||
debug_assert!(
|
||||
node_comments
|
||||
.dangling
|
||||
.iter()
|
||||
.all(SourceComment::is_formatted),
|
||||
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
|
||||
);
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
emit_source_position.then_some(source_position(node.end())),
|
||||
trailing_comments(node_comments.trailing)
|
||||
]
|
||||
)
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
emit_source_position.then_some(source_position(node.end())),
|
||||
trailing_comments(node_comments.trailing)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/// Formats the node's fields.
|
||||
fn fmt_fields(&self, item: &N, f: &mut PyFormatter) -> FormatResult<()>;
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
_trailing_comments: &[SourceComment],
|
||||
_context: &PyFormatContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, salsa::Update, PartialEq, Eq)]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::Decorator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::verbatim::verbatim_text;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -11,26 +12,27 @@ pub struct FormatDecorator;
|
||||
|
||||
impl FormatNodeRule<Decorator> for FormatDecorator {
|
||||
fn fmt_fields(&self, item: &Decorator, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let Decorator {
|
||||
expression,
|
||||
range: _,
|
||||
node_index: _,
|
||||
} = item;
|
||||
let comments = f.context().comments();
|
||||
let trailing = comments.trailing(item);
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token("@"),
|
||||
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
|
||||
]
|
||||
)
|
||||
}
|
||||
if has_skip_comment(trailing, f.context().source()) {
|
||||
comments.mark_verbatim_node_comments_formatted(item.into());
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
verbatim_text(item.range()).fmt(f)
|
||||
} else {
|
||||
let Decorator {
|
||||
expression,
|
||||
range: _,
|
||||
node_index: _,
|
||||
} = item;
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token("@"),
|
||||
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use crate::comments::Comments;
|
||||
use crate::context::{IndentLevel, NodeLevel};
|
||||
use crate::prelude::*;
|
||||
use crate::statement::suite::DocstringStmt;
|
||||
use crate::statement::suite::{DocstringStmt, skip_range};
|
||||
use crate::verbatim::{ends_suppression, starts_suppression};
|
||||
use crate::{FormatModuleError, PyFormatOptions, format_module_source};
|
||||
|
||||
@@ -251,7 +251,29 @@ impl<'ast> SourceOrderVisitor<'ast> for FindEnclosingNode<'_, 'ast> {
|
||||
// We only visit statements that aren't suppressed that's why we don't need to track the suppression
|
||||
// state in a stack. Assert that this assumption is safe.
|
||||
debug_assert!(self.suppressed.is_no());
|
||||
walk_body(self, body);
|
||||
|
||||
let mut iter = body.iter();
|
||||
|
||||
while let Some(stmt) = iter.next() {
|
||||
// If the range intersects a skip range then we need to
|
||||
// format the entire suite to properly handle the case
|
||||
// where a `fmt: skip` affects multiple statements.
|
||||
//
|
||||
// For example, in the case
|
||||
//
|
||||
// ```
|
||||
// <RANGE_START>x=1<RANGE_END>;x=2 # fmt: skip
|
||||
// ```
|
||||
//
|
||||
// the statement `x=1` does not "know" that it is
|
||||
// suppressed, but the suite does.
|
||||
if let Some(verbatim_range) = skip_range(stmt, iter.as_slice(), self.context)
|
||||
&& verbatim_range.intersect(self.range).is_some()
|
||||
{
|
||||
break;
|
||||
}
|
||||
self.visit_stmt(stmt);
|
||||
}
|
||||
self.suppressed = Suppressed::No;
|
||||
}
|
||||
}
|
||||
@@ -561,7 +583,7 @@ impl NarrowRange<'_> {
|
||||
}
|
||||
|
||||
pub(crate) const fn is_logical_line(node: AnyNodeRef) -> bool {
|
||||
// Make sure to update [`FormatEnclosingLine`] when changing this.
|
||||
// Make sure to update [`FormatEnclosingNode`] when changing this.
|
||||
node.is_statement()
|
||||
|| node.is_decorator()
|
||||
|| node.is_except_handler()
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtAnnAssign;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::is_splittable_expression;
|
||||
use crate::expression::parentheses::Parentheses;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::{
|
||||
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
|
||||
};
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAnnAssign;
|
||||
@@ -84,12 +83,4 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ use ruff_formatter::prelude::{space, token};
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtAssert;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAssert;
|
||||
@@ -41,12 +40,4 @@ impl FormatNodeRule<StmtAssert> for FormatStmtAssert {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::expression::{
|
||||
maybe_parenthesize_expression,
|
||||
};
|
||||
use crate::other::interpolated_string::InterpolatedStringLayout;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_parenthesize_lambda_bodies_enabled;
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::string::StringLikeExtensions;
|
||||
@@ -26,7 +27,6 @@ use crate::string::implicit::{
|
||||
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
|
||||
ImplicitConcatenatedLayout,
|
||||
};
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAssign;
|
||||
@@ -104,14 +104,6 @@ impl FormatNodeRule<StmtAssign> for FormatStmtAssign {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a single target with the equal operator.
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtAugAssign;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::parentheses::is_expression_parenthesized;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::{
|
||||
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
|
||||
has_target_own_parentheses,
|
||||
};
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::{AsFormat, FormatNodeRule};
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAugAssign;
|
||||
@@ -62,12 +61,4 @@ impl FormatNodeRule<StmtAugAssign> for FormatStmtAugAssign {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::StmtBreak;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtBreak;
|
||||
@@ -10,12 +9,4 @@ impl FormatNodeRule<StmtBreak> for FormatStmtBreak {
|
||||
fn fmt_fields(&self, _item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
token("break").fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::StmtContinue;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtContinue;
|
||||
@@ -10,12 +9,4 @@ impl FormatNodeRule<StmtContinue> for FormatStmtContinue {
|
||||
fn fmt_fields(&self, _item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
token("continue").fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use ruff_python_ast::StmtDelete;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::builders::{PyFormatterExtensions, parenthesize_if_expands};
|
||||
use crate::comments::{SourceComment, dangling_node_comments};
|
||||
use crate::comments::dangling_node_comments;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtDelete;
|
||||
@@ -57,12 +57,4 @@ impl FormatNodeRule<StmtDelete> for FormatStmtDelete {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, Operator, StmtExpr};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtExpr;
|
||||
@@ -30,14 +28,6 @@ impl FormatNodeRule<StmtExpr> for FormatStmtExpr {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_arithmetic_like(expression: &Expr) -> bool {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::StmtGlobal;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::has_skip_comment;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -47,12 +45,4 @@ impl FormatNodeRule<StmtGlobal> for FormatStmtGlobal {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::StmtImport;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtImport;
|
||||
@@ -21,12 +20,4 @@ impl FormatNodeRule<StmtImport> for FormatStmtImport {
|
||||
});
|
||||
write!(f, [token("import"), space(), names])
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ use ruff_python_ast::StmtImportFrom;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::builders::{PyFormatterExtensions, TrailingComma, parenthesize_if_expands};
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::parentheses::parenthesized;
|
||||
use crate::has_skip_comment;
|
||||
use crate::other::identifier::DotDelimitedIdentifier;
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -72,12 +70,4 @@ impl FormatNodeRule<StmtImportFrom> for FormatStmtImportFrom {
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use ruff_python_ast::StmtIpyEscapeCommand;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtIpyEscapeCommand;
|
||||
@@ -11,12 +10,4 @@ impl FormatNodeRule<StmtIpyEscapeCommand> for FormatStmtIpyEscapeCommand {
|
||||
fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
source_text_slice(item.range()).fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::StmtNonlocal;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::has_skip_comment;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -47,12 +45,4 @@ impl FormatNodeRule<StmtNonlocal> for FormatStmtNonlocal {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_python_ast::StmtPass;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtPass;
|
||||
@@ -10,12 +9,4 @@ impl FormatNodeRule<StmtPass> for FormatStmtPass {
|
||||
fn fmt_fields(&self, _item: &StmtPass, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
token("pass").fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtRaise;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtRaise;
|
||||
@@ -43,12 +42,4 @@ impl FormatNodeRule<StmtRaise> for FormatStmtRaise {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{Expr, StmtReturn};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::expr_tuple::TupleParentheses;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::FormatStatementsLastExpression;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtReturn;
|
||||
@@ -43,12 +42,4 @@ impl FormatNodeRule<StmtReturn> for FormatStmtReturn {
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtTypeAlias;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::{
|
||||
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
|
||||
};
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtTypeAlias;
|
||||
@@ -42,12 +41,4 @@ impl FormatNodeRule<StmtTypeAlias> for FormatStmtTypeAlias {
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ use ruff_formatter::{
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite};
|
||||
use ruff_python_ast::{AnyNodeRef, StmtExpr};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, lines_before};
|
||||
use ruff_python_trivia::{
|
||||
SimpleTokenKind, SimpleTokenizer, lines_after, lines_after_ignoring_end_of_line_trivia,
|
||||
lines_before,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::comments::{
|
||||
Comments, LeadingDanglingTrailingComments, leading_comments, trailing_comments,
|
||||
Comments, LeadingDanglingTrailingComments, has_skip_comment, leading_comments,
|
||||
trailing_comments,
|
||||
};
|
||||
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
@@ -16,9 +20,9 @@ use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled,
|
||||
};
|
||||
use crate::statement::stmt_expr::FormatStmtExpr;
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::verbatim::{
|
||||
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
|
||||
write_skipped_statements, write_suppressed_statements_starting_with_leading_comment,
|
||||
write_suppressed_statements_starting_with_trailing_comment,
|
||||
};
|
||||
|
||||
@@ -152,7 +156,21 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
|
||||
let first_comments = comments.leading_dangling_trailing(first);
|
||||
|
||||
let (mut preceding, mut empty_line_after_docstring) = if first_comments
|
||||
let (mut preceding, mut empty_line_after_docstring) = if let Some(verbatim_range) =
|
||||
skip_range(first.statement(), iter.as_slice(), f.context())
|
||||
{
|
||||
let preceding =
|
||||
write_skipped_statements(first.statement(), &mut iter, verbatim_range, f)?;
|
||||
|
||||
// Insert a newline after a module level docstring, but treat
|
||||
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
|
||||
let empty_line_after_docstring =
|
||||
matches!(self.kind, SuiteKind::TopLevel | SuiteKind::Class)
|
||||
&& DocstringStmt::try_from_statement(preceding, self.kind, f.context())
|
||||
.is_some();
|
||||
|
||||
(preceding, empty_line_after_docstring)
|
||||
} else if first_comments
|
||||
.leading
|
||||
.iter()
|
||||
.any(|comment| comment.is_suppression_off_comment(source))
|
||||
@@ -391,7 +409,10 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
}
|
||||
}
|
||||
|
||||
if following_comments
|
||||
if let Some(verbatim_range) = skip_range(following, iter.as_slice(), f.context()) {
|
||||
preceding = write_skipped_statements(following, &mut iter, verbatim_range, f)?;
|
||||
preceding_comments = comments.leading_dangling_trailing(preceding);
|
||||
} else if following_comments
|
||||
.leading
|
||||
.iter()
|
||||
.any(|comment| comment.is_suppression_off_comment(source))
|
||||
@@ -840,61 +861,57 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
|
||||
let comments = f.context().comments().clone();
|
||||
let node_comments = comments.leading_dangling_trailing(self.docstring);
|
||||
|
||||
if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) {
|
||||
suppressed_node(self.docstring).fmt(f)
|
||||
} else {
|
||||
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
|
||||
let string_literal = self
|
||||
.docstring
|
||||
.as_expr_stmt()
|
||||
.unwrap()
|
||||
.value
|
||||
.as_string_literal_expr()
|
||||
.unwrap();
|
||||
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
|
||||
let string_literal = self
|
||||
.docstring
|
||||
.as_expr_stmt()
|
||||
.unwrap()
|
||||
.value
|
||||
.as_string_literal_expr()
|
||||
.unwrap();
|
||||
|
||||
// We format the expression, but the statement carries the comments
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(node_comments.leading),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.start())),
|
||||
string_literal
|
||||
.format()
|
||||
.with_options(StringLiteralKind::Docstring),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.end())),
|
||||
]
|
||||
)?;
|
||||
// We format the expression, but the statement carries the comments
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(node_comments.leading),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.start())),
|
||||
string_literal
|
||||
.format()
|
||||
.with_options(StringLiteralKind::Docstring),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.end())),
|
||||
]
|
||||
)?;
|
||||
|
||||
if self.suite_kind == SuiteKind::Class {
|
||||
// Comments after class docstrings need a newline between the docstring and the
|
||||
// comment (https://github.com/astral-sh/ruff/issues/7948).
|
||||
// ```python
|
||||
// class ModuleBrowser:
|
||||
// """Browse module classes and functions in IDLE."""
|
||||
// # ^ Insert a newline above here
|
||||
//
|
||||
// def __init__(self, master, path, *, _htest=False, _utest=False):
|
||||
// pass
|
||||
// ```
|
||||
if let Some(own_line) = node_comments
|
||||
.trailing
|
||||
.iter()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
if lines_before(own_line.start(), f.context().source()) < 2 {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
if self.suite_kind == SuiteKind::Class {
|
||||
// Comments after class docstrings need a newline between the docstring and the
|
||||
// comment (https://github.com/astral-sh/ruff/issues/7948).
|
||||
// ```python
|
||||
// class ModuleBrowser:
|
||||
// """Browse module classes and functions in IDLE."""
|
||||
// # ^ Insert a newline above here
|
||||
//
|
||||
// def __init__(self, master, path, *, _htest=False, _utest=False):
|
||||
// pass
|
||||
// ```
|
||||
if let Some(own_line) = node_comments
|
||||
.trailing
|
||||
.iter()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
if lines_before(own_line.start(), f.context().source()) < 2 {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
trailing_comments(node_comments.trailing).fmt(f)
|
||||
}
|
||||
|
||||
trailing_comments(node_comments.trailing).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,6 +955,58 @@ impl Format<PyFormatContext<'_>> for SuiteChildStatement<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn skip_range(
|
||||
first: &Stmt,
|
||||
statements: &[Stmt],
|
||||
context: &PyFormatContext,
|
||||
) -> Option<TextRange> {
|
||||
let start = first.start();
|
||||
let mut last_statement = first;
|
||||
|
||||
let comments = context.comments();
|
||||
let source = context.source();
|
||||
|
||||
for statement in statements {
|
||||
if new_logical_line_between_statements(
|
||||
source,
|
||||
TextRange::new(last_statement.end(), statement.start()),
|
||||
) {
|
||||
break;
|
||||
}
|
||||
last_statement = statement;
|
||||
}
|
||||
|
||||
if has_skip_comment(comments.trailing(last_statement), source) {
|
||||
Some(TextRange::new(
|
||||
start,
|
||||
trailing_semicolon(last_statement.into(), source)
|
||||
.map_or_else(|| last_statement.end(), ruff_text_size::TextRange::end),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn new_logical_line_between_statements(source: &str, between_statement_range: TextRange) -> bool {
|
||||
let mut tokenizer = SimpleTokenizer::new(source, between_statement_range).map(|tok| tok.kind());
|
||||
|
||||
while let Some(token) = tokenizer.next() {
|
||||
match token {
|
||||
SimpleTokenKind::Continuation => {
|
||||
tokenizer.next();
|
||||
}
|
||||
SimpleTokenKind::Newline => {
|
||||
return true;
|
||||
}
|
||||
// Since we are between statements, there are
|
||||
// no non-trivia tokens, so there is no need to check
|
||||
// for these and do an early return.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_formatter::format;
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
use std::slice::Iter;
|
||||
|
||||
use itertools::PeekingNext;
|
||||
use ruff_formatter::{FormatError, write};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::Stmt;
|
||||
@@ -451,6 +452,40 @@ fn write_suppressed_statements<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub(crate) fn write_skipped_statements<'a>(
|
||||
first_skipped: &'a Stmt,
|
||||
statements: &mut std::slice::Iter<'a, Stmt>,
|
||||
verbatim_range: TextRange,
|
||||
f: &mut PyFormatter,
|
||||
) -> FormatResult<&'a Stmt> {
|
||||
let comments = f.context().comments().clone();
|
||||
comments.mark_verbatim_node_comments_formatted(first_skipped.into());
|
||||
|
||||
let mut preceding = first_skipped;
|
||||
|
||||
while let Some(prec) = statements.peeking_next(|next| next.end() <= verbatim_range.end()) {
|
||||
comments.mark_verbatim_node_comments_formatted(prec.into());
|
||||
preceding = prec;
|
||||
}
|
||||
|
||||
let first_leading = comments.leading(first_skipped);
|
||||
let preceding_trailing = comments.trailing(preceding);
|
||||
|
||||
// Write the outer comments and format the node as verbatim
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(first_leading),
|
||||
source_position(verbatim_range.start()),
|
||||
verbatim_text(verbatim_range),
|
||||
source_position(verbatim_range.end()),
|
||||
trailing_comments(preceding_trailing)
|
||||
]
|
||||
)?;
|
||||
Ok(preceding)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum InSuppression {
|
||||
No,
|
||||
@@ -893,65 +928,6 @@ impl Format<PyFormatContext<'_>> for VerbatimText {
|
||||
}
|
||||
}
|
||||
|
||||
/// Disables formatting for `node` and instead uses the same formatting as the node has in source.
|
||||
///
|
||||
/// The `node` gets indented as any formatted node to avoid syntax errors when the indentation string changes (e.g. from 2 spaces to 4).
|
||||
/// The `node`s leading and trailing comments are formatted as usual, except if they fall into the suppressed node's range.
|
||||
#[cold]
|
||||
pub(crate) fn suppressed_node<'a, N>(node: N) -> FormatSuppressedNode<'a>
|
||||
where
|
||||
N: Into<AnyNodeRef<'a>>,
|
||||
{
|
||||
FormatSuppressedNode { node: node.into() }
|
||||
}
|
||||
|
||||
pub(crate) struct FormatSuppressedNode<'a> {
|
||||
node: AnyNodeRef<'a>,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatSuppressedNode<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
let comments = f.context().comments().clone();
|
||||
let node_comments = comments.leading_dangling_trailing(self.node);
|
||||
|
||||
// Mark all comments as formatted that fall into the node range
|
||||
for comment in node_comments.leading {
|
||||
if comment.start() > self.node.start() {
|
||||
comment.mark_formatted();
|
||||
}
|
||||
}
|
||||
|
||||
for comment in node_comments.trailing {
|
||||
if comment.start() < self.node.end() {
|
||||
comment.mark_formatted();
|
||||
}
|
||||
}
|
||||
|
||||
// Some statements may end with a semicolon. Preserve the semicolon
|
||||
let semicolon_range = self
|
||||
.node
|
||||
.is_statement()
|
||||
.then(|| trailing_semicolon(self.node, f.context().source()))
|
||||
.flatten();
|
||||
let verbatim_range = semicolon_range.map_or(self.node.range(), |semicolon| {
|
||||
TextRange::new(self.node.start(), semicolon.end())
|
||||
});
|
||||
comments.mark_verbatim_node_comments_formatted(self.node);
|
||||
|
||||
// Write the outer comments and format the node as verbatim
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(node_comments.leading),
|
||||
source_position(verbatim_range.start()),
|
||||
verbatim_text(verbatim_range),
|
||||
source_position(verbatim_range.end()),
|
||||
trailing_comments(node_comments.trailing)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub(crate) fn write_suppressed_clause_header(
|
||||
header: ClauseHeader,
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
class Simple:
|
||||
x=1
|
||||
x=2 # fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class TrailingSemicolon:
|
||||
x=1
|
||||
x=2;x=3 ; # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
x=1;
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
def foo(): y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
class MultiLineSemicolon:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
]; x=2 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAfter:
|
||||
x=1
|
||||
x = ['a']\
|
||||
; y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonBefore:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewline:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewlineAndComment:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
# 1
|
||||
y=1 # fmt: skip
|
||||
|
||||
class RepeatedLineContinuation:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
\
|
||||
\
|
||||
y=1 # fmt: skip
|
||||
|
||||
class MultiLineSemicolonComments:
|
||||
x=1
|
||||
# 1
|
||||
x = [ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
# 4
|
||||
]; x=2 # 5 # fmt: skip # 6
|
||||
|
||||
class DocstringSkipped:
|
||||
'''This is a docstring''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class MultilineDocstringSkipped:
|
||||
'''This is a docstring
|
||||
''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class FirstStatementNewlines:
|
||||
|
||||
|
||||
|
||||
|
||||
x=1 # fmt: skip
|
||||
|
||||
class ChainingSemicolons:
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
];x=1;x=[
|
||||
'1',
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # fmt: skip
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
class MixingCompound:
|
||||
def foo(): bar(); import zoo # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17331
|
||||
def main() -> None:
|
||||
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11430
|
||||
print(); print() # noqa # fmt: skip
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
class Simple:
|
||||
x = 1
|
||||
x=2 # fmt: skip
|
||||
x = 3
|
||||
|
||||
|
||||
class Semicolon:
|
||||
x = 1
|
||||
x=2;x=3 # fmt: skip
|
||||
x = 4
|
||||
|
||||
|
||||
class TrailingSemicolon:
|
||||
x = 1
|
||||
x=2;x=3 ; # fmt: skip
|
||||
x = 4
|
||||
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
x = 1
|
||||
x=2;x=3 # fmt: skip
|
||||
x = 4
|
||||
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x = 1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x = 5
|
||||
|
||||
|
||||
class CompoundInSuite:
|
||||
x = 1
|
||||
|
||||
def foo(): y=1 # fmt: skip
|
||||
|
||||
x = 2
|
||||
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x = 1
|
||||
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
|
||||
x = 2
|
||||
|
||||
|
||||
class MultiLineSkip:
|
||||
x = 1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class MultiLineSemicolon:
|
||||
x = 1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
]; x=2 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonAfter:
|
||||
x = 1
|
||||
x = ['a']\
|
||||
; y=1 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonBefore:
|
||||
x = 1
|
||||
x = ['a']; \
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonAndNewline:
|
||||
x = 1
|
||||
x = ["a"]
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonAndNewlineAndComment:
|
||||
x = 1
|
||||
x = ["a"]
|
||||
# 1
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class RepeatedLineContinuation:
|
||||
x = 1
|
||||
x = ['a']; \
|
||||
\
|
||||
\
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class MultiLineSemicolonComments:
|
||||
x = 1
|
||||
# 1
|
||||
x = [ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
# 4
|
||||
]; x=2 # 5 # fmt: skip # 6
|
||||
|
||||
|
||||
class DocstringSkipped:
|
||||
'''This is a docstring''' # fmt: skip
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
class MultilineDocstringSkipped:
|
||||
'''This is a docstring
|
||||
''' # fmt: skip
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
class FirstStatementNewlines:
|
||||
x=1 # fmt: skip
|
||||
|
||||
|
||||
class ChainingSemicolons:
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
];x=1;x=[
|
||||
'1',
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
|
||||
class MixingCompound:
|
||||
def foo(): bar(); import zoo # fmt: skip
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17331
|
||||
def main() -> None:
|
||||
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11430
|
||||
print(); print() # noqa # fmt: skip
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# 0
|
||||
'''Module docstring
|
||||
multiple lines''' # 1 # fmt: skip # 2
|
||||
|
||||
import a;import b; from c import (
|
||||
x,y,z
|
||||
); import f # fmt: skip
|
||||
|
||||
x=1;x=2;x=3;x=4 # fmt: skip
|
||||
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
def foo(): x=[
|
||||
'1',
|
||||
'2',
|
||||
];x=1 # fmt: skip
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
# 0
|
||||
'''Module docstring
|
||||
multiple lines''' # 1 # fmt: skip # 2
|
||||
|
||||
import a;import b; from c import (
|
||||
x,y,z
|
||||
); import f # fmt: skip
|
||||
|
||||
x=1;x=2;x=3;x=4 # fmt: skip
|
||||
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
|
||||
def foo():
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
];x=1 # fmt: skip
|
||||
```
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
class Simple:
|
||||
# Range comprises skip range
|
||||
x=1
|
||||
<RANGE_START>x=2 <RANGE_END># fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
# Range is part of skip range
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatFirst:
|
||||
x=1
|
||||
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatMiddle:
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on right side
|
||||
<RANGE_START>x=1;
|
||||
x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on left side
|
||||
x=1;
|
||||
x=2;<RANGE_START>x=3 # fmt: skip
|
||||
x=4<RANGE_END>
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
# Range inside statement
|
||||
x=1
|
||||
x = <RANGE_START>[
|
||||
'1',
|
||||
'2',<RANGE_END>
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3<RANGE_START>
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
class Simple:
|
||||
# Range comprises skip range
|
||||
x=1
|
||||
x=2 # fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
# Range is part of skip range
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatFirst:
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatMiddle:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on right side
|
||||
x = 1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on left side
|
||||
x=1;
|
||||
x=2;x=3 # fmt: skip
|
||||
x = 4
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
def foo(): y=1 # fmt: skip
|
||||
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
# Range inside statement
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
```
|
||||
@@ -34,12 +34,12 @@ cargo install cargo-insta
|
||||
You'll need [uv](https://docs.astral.sh/uv/getting-started/installation/) (or `pipx` and `pip`) to
|
||||
run Python utility commands.
|
||||
|
||||
You can optionally install pre-commit hooks to automatically run the validation checks
|
||||
You can optionally install hooks to automatically run the validation checks
|
||||
when making a commit:
|
||||
|
||||
```shell
|
||||
uv tool install pre-commit
|
||||
pre-commit install
|
||||
uv tool install prek
|
||||
prek install
|
||||
```
|
||||
|
||||
We recommend [nextest](https://nexte.st/) to run ty's test suite (via `cargo nextest run`),
|
||||
@@ -66,7 +66,7 @@ and that it passes both the lint and test validation checks:
|
||||
```shell
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
|
||||
cargo test # Rust testing
|
||||
uvx pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
|
||||
uvx prek run -a # Rust and Python formatting, Markdown and Python linting, etc.
|
||||
```
|
||||
|
||||
These checks will run on GitHub Actions when you open your pull request, but running them locally
|
||||
|
||||
280
crates/ty/docs/rules.md
generated
280
crates/ty/docs/rules.md
generated
@@ -8,7 +8,7 @@
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L540" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L139" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L142" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.7">0.0.7</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-top-callable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L160" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ def f(x: object):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L211" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ f(int) # error
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L237" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ a = 1
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L259" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L262" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L288" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ class B(A): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L311" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L314" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ type B = A
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L355" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L358" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L333" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L336" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ false positives it can produce.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L376" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L379" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -373,7 +373,7 @@ class B(A, A): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L397" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L400" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L623" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -559,7 +559,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L647" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -579,13 +579,47 @@ t = (0, 1, 2)
|
||||
t[3] # IndexError: tuple index out of range
|
||||
```
|
||||
|
||||
## `ineffective-final`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.33">0.0.1-alpha.33</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ineffective-final" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1793" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for calls to `final()` that type checkers cannot interpret.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
The `final()` function is designed to be used as a decorator. When called directly
|
||||
as a function (e.g., `final(type(...))`), type checkers will not understand the
|
||||
application of `final` and will not prevent subclassing.
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
```python
|
||||
from typing import final
|
||||
|
||||
# Incorrect: type checkers will not prevent subclassing
|
||||
MyClass = final(type("MyClass", (), {}))
|
||||
|
||||
# Correct: use `final` as a decorator
|
||||
@final
|
||||
class MyClass: ...
|
||||
```
|
||||
|
||||
## `instance-layout-conflict`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L429" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L432" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -674,7 +708,7 @@ an atypical memory layout.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L701" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L704" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -701,7 +735,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L741" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L744" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -729,7 +763,7 @@ a: int = ''
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2044" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2151" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -763,7 +797,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L763" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -799,7 +833,7 @@ asyncio.run(main())
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L793" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L796" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -823,7 +857,7 @@ class A(42): ... # error: [invalid-base]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L844" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L881" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -850,7 +884,7 @@ with 1:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L902" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -879,7 +913,7 @@ a: str
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L888" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L925" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -923,7 +957,7 @@ except ZeroDivisionError:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1714" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1821" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -965,7 +999,7 @@ class D(A):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.35">0.0.1-alpha.35</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2295" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2402" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1009,7 +1043,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1003" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1041,6 +1075,55 @@ class D(Generic[U, T]): ...
|
||||
|
||||
- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)
|
||||
|
||||
## `invalid-generic-enum`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.12">0.0.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-enum" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L961" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for enum classes that are also generic.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Enum classes cannot be generic. Python does not support generic enums:
|
||||
attempting to create one will either result in an immediate `TypeError`
|
||||
at runtime, or will create a class that cannot be specialized in the way
|
||||
that a normal generic class can.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: enum class cannot be generic (class creation fails with `TypeError`)
|
||||
class E[T](Enum):
|
||||
A = 1
|
||||
|
||||
# error: enum class cannot be generic (class creation fails with `TypeError`)
|
||||
class F(Enum, Generic[T]):
|
||||
A = 1
|
||||
|
||||
# error: enum class cannot be generic -- the class creation does not immediately fail...
|
||||
class G(Generic[T], Enum):
|
||||
A = 1
|
||||
|
||||
# ...but this raises `KeyError`:
|
||||
x: G[int]
|
||||
```
|
||||
|
||||
**References**
|
||||
|
||||
- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)
|
||||
|
||||
## `invalid-ignore-comment`
|
||||
|
||||
<small>
|
||||
@@ -1077,7 +1160,7 @@ a = 20 / 0 # type: ignore
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L671" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1116,7 +1199,7 @@ carol = Person(name="Carol", age=25) # typo!
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L955" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1034" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1151,7 +1234,7 @@ def f(t: TypeVar("U")): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1052" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1131" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1185,7 +1268,7 @@ class B(metaclass=f): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2197" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2304" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1292,7 +1375,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L575" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L578" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1346,7 +1429,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1028" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1376,7 +1459,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1079" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1158" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1426,7 +1509,7 @@ def foo(x: int) -> int: ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1178" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1257" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1452,7 +1535,7 @@ def f(a: int = ''): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1483,7 +1566,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L514" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1517,7 +1600,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1277" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1566,7 +1649,7 @@ def g():
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L722" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L725" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1591,7 +1674,7 @@ def func() -> int:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1241" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1320" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1687,7 +1770,7 @@ class C: ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.10">0.0.10</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-total-ordering" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2333" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2440" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1733,7 +1816,7 @@ class MyClass:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1007" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1760,7 +1843,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1473" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1552" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1807,7 +1890,7 @@ Bar[int] # error: too few arguments
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1280" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1359" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1837,7 +1920,7 @@ TYPE_CHECKING = ''
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1304" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1383" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1867,7 +1950,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1356" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1435" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1901,7 +1984,7 @@ f(10) # Error
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1328" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1407" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1935,7 +2018,7 @@ class C:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1384" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1970,7 +2053,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.9">0.0.9</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-typed-dict-statement" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2172" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2279" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2001,7 +2084,7 @@ class Foo(TypedDict):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1413" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1492" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2026,7 +2109,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2145" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2252" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2059,7 +2142,7 @@ alice["age"] # KeyError
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1432" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1511" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2088,7 +2171,7 @@ func("string") # error: [no-matching-overload]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1514" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1593" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2114,7 +2197,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-subscriptable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1455" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1534" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2138,7 +2221,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1687" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1766" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2171,7 +2254,7 @@ class B(A):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1644" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2198,7 +2281,7 @@ f(1, x=2) # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1898" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2005" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2225,7 +2308,7 @@ f(x=1) # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1665" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2253,7 +2336,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L182" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L185" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2285,7 +2368,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1608" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1687" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2322,7 +2405,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1638" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1717" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2386,7 +2469,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2072" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2179" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2413,7 +2496,7 @@ cast(int, f()) # Redundant
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2020" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2127" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2443,7 +2526,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1664" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1743" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2472,7 +2555,7 @@ class B(A): ... # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1832" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1939" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2506,7 +2589,7 @@ class F(NamedTuple):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1772" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1879" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2533,7 +2616,7 @@ f("foo") # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1750" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2561,7 +2644,7 @@ def _(x: int):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1793" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1900" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2607,7 +2690,7 @@ class A:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1859" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1966" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2631,7 +2714,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1877" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1984" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2658,7 +2741,7 @@ f(x=1, y=2) # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1919" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2026" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2686,7 +2769,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2093" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2200" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2744,7 +2827,7 @@ def g():
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1941" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2048" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2769,7 +2852,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1960" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2067" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2794,7 +2877,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L814" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2833,7 +2916,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1534" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1613" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2864,13 +2947,54 @@ not b1 # exception raised here
|
||||
b1 < b2 < b1 # exception raised here
|
||||
```
|
||||
|
||||
## `unsupported-dynamic-base`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-dynamic-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L847" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for dynamic class definitions (using `type()`) that have bases
|
||||
which are unsupported by ty.
|
||||
|
||||
This is equivalent to [`unsupported-base`] but applies to classes created
|
||||
via `type()` rather than `class` statements.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
If a dynamically created class has a base that is an unsupported type
|
||||
such as `type[T]`, ty will not be able to resolve the
|
||||
[method resolution order] (MRO) for the class. This may lead to an inferior
|
||||
understanding of your codebase and unpredictable type-checking behavior.
|
||||
|
||||
**Default level**
|
||||
|
||||
This rule is disabled by default because it will not cause a runtime error,
|
||||
and may be noisy on codebases that use `type()` in highly dynamic ways.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
def factory(base: type[Base]) -> type:
|
||||
# `base` has type `type[Base]`, not `type[Base]` itself
|
||||
return type("Dynamic", (base,), {}) # error: [unsupported-dynamic-base]
|
||||
```
|
||||
|
||||
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
[`unsupported-base`]: https://docs.astral.sh/ty/rules/unsupported-base
|
||||
|
||||
## `unsupported-operator`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1979" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2086" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2934,7 +3058,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1122" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1201" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2997,7 +3121,7 @@ def foo(x: int | str) -> int | str:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2001" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2108" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
||||
@@ -2585,7 +2585,7 @@ type<CURSOR>
|
||||
|
||||
assert_snapshot!(
|
||||
test.type_signatures().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
@"
|
||||
type :: <class 'type'>
|
||||
TypeError :: <class 'TypeError'>
|
||||
",
|
||||
@@ -3586,7 +3586,7 @@ quux.<CURSOR>
|
||||
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self@__new__
|
||||
__new__ :: def __new__[Self](cls) -> Self
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
@@ -3667,19 +3667,19 @@ C.<CURSOR>
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self@__new__
|
||||
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
||||
__new__ :: def __new__[Self](cls) -> Self
|
||||
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self
|
||||
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
||||
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: bound method <class 'C'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
||||
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'C'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3737,18 +3737,18 @@ Meta.<CURSOR>
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__or__ :: def __or__[Self](self: Self@__or__, value: Any, /) -> UnionType | Self@__or__
|
||||
__or__ :: def __or__[Self](self: Self, value: Any, /) -> UnionType | Self
|
||||
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: def __ror__[Self](self: Self@__ror__, value: Any, /) -> UnionType | Self@__ror__
|
||||
__ror__ :: def __ror__[Self](self: Self, value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
|
||||
__subclasses__ :: def __subclasses__[Self](self: Self@__subclasses__) -> list[Self@__subclasses__]
|
||||
__subclasses__ :: def __subclasses__[Self](self: Self) -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Meta'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3866,19 +3866,19 @@ Quux.<CURSOR>
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self@__new__
|
||||
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
||||
__new__ :: def __new__[Self](cls) -> Self
|
||||
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self
|
||||
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
||||
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: bound method <class 'Quux'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
||||
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Quux'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3919,8 +3919,8 @@ Answer.<CURSOR>
|
||||
__bool__ :: bound method <class 'Answer'>.__bool__() -> Literal[True]
|
||||
__class__ :: <class 'EnumMeta'>
|
||||
__contains__ :: bound method <class 'Answer'>.__contains__(value: object) -> bool
|
||||
__copy__ :: def __copy__(self) -> Self@__copy__
|
||||
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__
|
||||
__copy__ :: def __copy__[Self](self) -> Self
|
||||
__deepcopy__ :: def __deepcopy__[Self](self, memo: Any) -> Self
|
||||
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dictoffset__ :: int
|
||||
@@ -3930,34 +3930,34 @@ Answer.<CURSOR>
|
||||
__flags__ :: int
|
||||
__format__ :: def __format__(self, format_spec: str) -> str
|
||||
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
||||
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT@__getitem__
|
||||
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT
|
||||
__getstate__ :: def __getstate__(self) -> object
|
||||
__hash__ :: def __hash__(self) -> int
|
||||
__init__ :: def __init__(self) -> None
|
||||
__init_subclass__ :: bound method <class 'Answer'>.__init_subclass__() -> None
|
||||
__instancecheck__ :: bound method <class 'Answer'>.__instancecheck__(instance: Any, /) -> bool
|
||||
__itemsize__ :: int
|
||||
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]
|
||||
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT]
|
||||
__len__ :: bound method <class 'Answer'>.__len__() -> int
|
||||
__members__ :: MappingProxyType[str, Answer]
|
||||
__module__ :: str
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls, value: object) -> Self@__new__
|
||||
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
||||
__new__ :: def __new__[Self](cls, value: object) -> Self
|
||||
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self
|
||||
__order__ :: str
|
||||
__prepare__ :: bound method <class 'EnumMeta'>.__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
|
||||
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
||||
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT]
|
||||
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
||||
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Answer'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3999,7 +3999,7 @@ quux.<CURSOR>
|
||||
index :: bound method Quux.index(value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int
|
||||
x :: int
|
||||
y :: str
|
||||
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]]
|
||||
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], [_T](value: tuple[_T, ...], /) -> tuple[int | str | _T, ...]]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type[Quux]
|
||||
__class_getitem__ :: bound method type[Quux].__class_getitem__(item: Any, /) -> GenericAlias
|
||||
@@ -4025,7 +4025,7 @@ quux.<CURSOR>
|
||||
__module__ :: str
|
||||
__mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...]
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: (x: int, y: str) -> None
|
||||
__new__ :: (x: int, y: str) -> Quux
|
||||
__orig_bases__ :: tuple[Any, ...]
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
@@ -8009,7 +8009,7 @@ my_list[0].remove<CURSOR>
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@r"
|
||||
@"
|
||||
removeprefix
|
||||
removesuffix
|
||||
",
|
||||
@@ -8030,7 +8030,7 @@ def f(x: Any | str):
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@r"
|
||||
@"
|
||||
removeprefix
|
||||
removesuffix
|
||||
",
|
||||
@@ -8052,13 +8052,24 @@ def f(x: Intersection[int, Any] | str):
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.build().snapshot(),
|
||||
@r"
|
||||
@"
|
||||
removeprefix
|
||||
removesuffix
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dunder_file_completion() {
|
||||
let builder = completion_test_builder("__fil<CURSOR>");
|
||||
|
||||
// __file__ should be `str` when accessed within a module, not `str | None`
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_auto_import().type_signatures().build().snapshot(),
|
||||
@"__file__ :: str",
|
||||
);
|
||||
}
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) completion test
|
||||
/// builder.
|
||||
///
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use regex::Regex;
|
||||
use ruff_python_trivia::{PythonWhitespace, leading_indentation};
|
||||
use ruff_source_file::UniversalNewlines;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -349,16 +350,37 @@ fn render_markdown(docstring: &str) -> String {
|
||||
| "versionchanged" | "version-changed" | "version-deprecated" | "deprecated"
|
||||
| "version-removed" | "versionremoved",
|
||||
) => {
|
||||
// Map version directives to human-readable phrases (matching Sphinx output)
|
||||
let pretty_directive = match directive.unwrap() {
|
||||
"versionadded" | "version-added" => Cow::Borrowed("Added in version"),
|
||||
"versionchanged" | "version-changed" => Cow::Borrowed("Changed in version"),
|
||||
"deprecated" | "version-deprecated" => {
|
||||
Cow::Borrowed("Deprecated since version")
|
||||
}
|
||||
"versionremoved" | "version-removed" => Cow::Borrowed("Removed in version"),
|
||||
other => Cow::Owned(
|
||||
other
|
||||
.char_indices()
|
||||
.map(|(index, c)| {
|
||||
if index == 0 {
|
||||
c.to_ascii_uppercase()
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
|
||||
// Render the argument of things like `.. version-added:: 4.0`
|
||||
let suffix = if let Some(lang) = lang {
|
||||
format!(" *{lang}*")
|
||||
format!(" {lang}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// We prepend without_directive here out of caution for preserving input.
|
||||
// This is probably gibberish/invalid syntax? But it's a no-op in normal cases.
|
||||
temp_owned_line =
|
||||
format!("**{without_directive}{}:**{suffix}", directive.unwrap());
|
||||
temp_owned_line = format!("**{without_directive}{pretty_directive}{suffix}:**");
|
||||
|
||||
line = temp_owned_line.as_str();
|
||||
None
|
||||
@@ -1076,7 +1098,7 @@ mod tests {
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
The thing you need to understand is that computers are hard.
|
||||
|
||||
**warning:**
|
||||
**Warning:**
|
||||
Now listen here buckaroo you might have seen me say computers are hard,
|
||||
and though "yeah I know computers are hard but NO you DON'T KNOW.
|
||||
|
||||
@@ -1115,12 +1137,12 @@ mod tests {
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
Some much-updated docs
|
||||
|
||||
**version-added:** *3.0*
|
||||
**Added in version 3.0:**
|
||||
Function added
|
||||
|
||||
**version-changed:** *4.0*
|
||||
**Changed in version 4.0:**
|
||||
The `spam` argument was added
|
||||
**version-changed:** *4.1*
|
||||
**Changed in version 4.1:**
|
||||
The `spam` argument is considered evil now.
|
||||
|
||||
You really shouldnt use it
|
||||
@@ -1141,7 +1163,7 @@ mod tests {
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
**wow this is some changes deprecated:** *1.2.3*
|
||||
**wow this is some changes Deprecated since version 1.2.3:**
|
||||
x = 2
|
||||
");
|
||||
}
|
||||
@@ -1151,7 +1173,7 @@ mod tests {
|
||||
fn explicit_markdown_block_with_ps1_contents() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
```python
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
@@ -1162,7 +1184,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
```python
|
||||
@@ -1179,7 +1201,7 @@ mod tests {
|
||||
fn explicit_markdown_block_with_underscore_contents_tick() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
`````python
|
||||
x_y = thing_do();
|
||||
``` # this should't close the fence!
|
||||
@@ -1189,7 +1211,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
`````python
|
||||
@@ -1205,7 +1227,7 @@ mod tests {
|
||||
fn explicit_markdown_block_with_underscore_contents_tilde() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
~~~~~python
|
||||
x_y = thing_do();
|
||||
~~~ # this should't close the fence!
|
||||
@@ -1215,7 +1237,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
~~~~~python
|
||||
@@ -1233,7 +1255,7 @@ mod tests {
|
||||
fn explicit_markdown_block_with_indent_tick() {
|
||||
let docstring = r#"
|
||||
My cool func...
|
||||
|
||||
|
||||
Returns:
|
||||
Some details
|
||||
`````python
|
||||
@@ -1246,7 +1268,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func...
|
||||
|
||||
Returns:
|
||||
@@ -1267,7 +1289,7 @@ mod tests {
|
||||
fn explicit_markdown_block_with_indent_tilde() {
|
||||
let docstring = r#"
|
||||
My cool func...
|
||||
|
||||
|
||||
Returns:
|
||||
Some details
|
||||
~~~~~~python
|
||||
@@ -1280,7 +1302,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func...
|
||||
|
||||
Returns:
|
||||
@@ -1299,14 +1321,14 @@ mod tests {
|
||||
fn explicit_markdown_block_with_unclosed_fence_tick() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
````python
|
||||
x_y = thing_do();
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
````python
|
||||
@@ -1320,14 +1342,14 @@ mod tests {
|
||||
fn explicit_markdown_block_with_unclosed_fence_tilde() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
~~~~~python
|
||||
x_y = thing_do();
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
~~~~~python
|
||||
@@ -1342,7 +1364,7 @@ mod tests {
|
||||
fn explicit_markdown_block_messy_corners_tick() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
``````we still think this is a codefence```
|
||||
x_y = thing_do();
|
||||
```````````` and are sloppy as heck with indentation and closing shrugggg
|
||||
@@ -1350,7 +1372,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
``````we still think this is a codefence```
|
||||
@@ -1365,7 +1387,7 @@ mod tests {
|
||||
fn explicit_markdown_block_messy_corners_tilde() {
|
||||
let docstring = r#"
|
||||
My cool func:
|
||||
|
||||
|
||||
~~~~~~we still think this is a codefence~~~
|
||||
x_y = thing_do();
|
||||
~~~~~~~~~~~~~ and are sloppy as heck with indentation and closing shrugggg
|
||||
@@ -1373,7 +1395,7 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
assert_snapshot!(docstring.render_markdown(), @"
|
||||
My cool func:
|
||||
|
||||
~~~~~~we still think this is a codefence~~~
|
||||
|
||||
@@ -231,7 +231,8 @@ impl<'db> Definitions<'db> {
|
||||
ty_python_semantic::types::TypeDefinition::Module(module) => {
|
||||
ResolvedDefinition::Module(module.file(db)?)
|
||||
}
|
||||
ty_python_semantic::types::TypeDefinition::Class(definition)
|
||||
ty_python_semantic::types::TypeDefinition::StaticClass(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::DynamicClass(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::Function(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
|
||||
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
|
||||
|
||||
@@ -1649,6 +1649,103 @@ Traceb<CURSOR>ackType
|
||||
assert_snapshot!(test.goto_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
/// goto-definition on a dynamic class literal (created via `type()`)
|
||||
#[test]
|
||||
fn goto_definition_dynamic_class_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
DynClass = type("DynClass", (), {})
|
||||
|
||||
x = DynCla<CURSOR>ss()
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Go to definition
|
||||
--> main.py:4:5
|
||||
|
|
||||
2 | DynClass = type("DynClass", (), {})
|
||||
3 |
|
||||
4 | x = DynClass()
|
||||
| ^^^^^^^^ Clicking here
|
||||
|
|
||||
info: Found 2 definitions
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | DynClass = type("DynClass", (), {})
|
||||
| --------
|
||||
3 |
|
||||
4 | x = DynClass()
|
||||
|
|
||||
::: stdlib/builtins.pyi:137:9
|
||||
|
|
||||
135 | def __class__(self, type: type[Self], /) -> None: ...
|
||||
136 | def __init__(self) -> None: ...
|
||||
137 | def __new__(cls) -> Self: ...
|
||||
| -------
|
||||
138 | # N.B. `object.__setattr__` and `object.__delattr__` are heavily special-cased by type checkers.
|
||||
139 | # Overriding them in subclasses has different semantics, even if the override has an identical signature.
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a dangling dynamic class literal (not assigned to a variable)
|
||||
#[test]
|
||||
fn goto_definition_dangling_dynamic_class_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
class Foo(type("Ba<CURSOR>r", (), {})):
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
/// goto-definition on a dynamic namedtuple class literal (created via `collections.namedtuple()`)
|
||||
#[test]
|
||||
fn goto_definition_dynamic_namedtuple_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from collections import namedtuple
|
||||
|
||||
Point = namedtuple("Point", ["x", "y"])
|
||||
|
||||
p = Poi<CURSOR>nt(1, 2)
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Go to definition
|
||||
--> main.py:6:5
|
||||
|
|
||||
4 | Point = namedtuple("Point", ["x", "y"])
|
||||
5 |
|
||||
6 | p = Point(1, 2)
|
||||
| ^^^^^ Clicking here
|
||||
|
|
||||
info: Found 1 definition
|
||||
--> main.py:4:1
|
||||
|
|
||||
2 | from collections import namedtuple
|
||||
3 |
|
||||
4 | Point = namedtuple("Point", ["x", "y"])
|
||||
| -----
|
||||
5 |
|
||||
6 | p = Point(1, 2)
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
// TODO: Should only list `a: int`
|
||||
#[test]
|
||||
fn redeclarations() {
|
||||
|
||||
@@ -4166,6 +4166,34 @@ def function():
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_dunder_file() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
__fil<CURSOR>e__
|
||||
"#,
|
||||
);
|
||||
|
||||
// __file__ should be `str` when accessed within a module, not `str | None`
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
str
|
||||
---------------------------------------------
|
||||
```python
|
||||
str
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | __file__
|
||||
| ^^^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn hover(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
@@ -456,6 +456,40 @@ reveal_type(int_container) # revealed: Container[int]
|
||||
reveal_type(int_container.set_value(1)) # revealed: Container[int]
|
||||
```
|
||||
|
||||
## Generic class with bounded type variable
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/2467>.
|
||||
|
||||
Calling a method on a generic class instance should work when the type parameter is specialized with
|
||||
a type that satisfies a bound.
|
||||
|
||||
```py
|
||||
from typing import NewType
|
||||
|
||||
class Base: ...
|
||||
|
||||
class C[T: Base]:
|
||||
x: T
|
||||
|
||||
def g(self) -> None:
|
||||
pass
|
||||
|
||||
# Calling a method on a specialized instance should not produce an error
|
||||
C[Base]().g()
|
||||
|
||||
# Test with a NewType bound
|
||||
K = NewType("K", int)
|
||||
|
||||
class D[T: K]:
|
||||
x: T
|
||||
|
||||
def h(self) -> None:
|
||||
pass
|
||||
|
||||
# Calling a method on a specialized instance should not produce an error
|
||||
D[K]().h()
|
||||
```
|
||||
|
||||
## Protocols
|
||||
|
||||
TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Unsupported special types
|
||||
|
||||
We do not understand the functional syntax for creating `NamedTuple`s, `TypedDict`s or `Enum`s yet.
|
||||
But we also do not emit false positives when these are used in type expressions.
|
||||
We do not understand the functional syntax for creating `TypedDict`s or `Enum`s yet. But we also do
|
||||
not emit false positives when these are used in type expressions.
|
||||
|
||||
```py
|
||||
import collections
|
||||
@@ -11,8 +11,6 @@ import typing
|
||||
MyEnum = enum.Enum("MyEnum", ["foo", "bar", "baz"])
|
||||
MyIntEnum = enum.IntEnum("MyIntEnum", ["foo", "bar", "baz"])
|
||||
MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int})
|
||||
MyNamedTuple1 = typing.NamedTuple("MyNamedTuple1", [("foo", int)])
|
||||
MyNamedTuple2 = collections.namedtuple("MyNamedTuple2", ["foo"])
|
||||
|
||||
def f(a: MyEnum, b: MyTypedDict, c: MyNamedTuple1, d: MyNamedTuple2): ...
|
||||
def f(a: MyEnum, b: MyTypedDict): ...
|
||||
```
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def _(flag: bool):
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8:15'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
|
||||
@@ -42,6 +42,12 @@ def f[T](x: T, cond: bool) -> T | list[T]:
|
||||
return x if cond else [x]
|
||||
|
||||
l5: int | list[int] = f(1, True)
|
||||
|
||||
a: list[int] = [1, 2, *(3, 4, 5)]
|
||||
reveal_type(a) # revealed: list[int]
|
||||
|
||||
b: list[list[int]] = [[1], [2], *([3], [4])]
|
||||
reveal_type(b) # revealed: list[list[int]]
|
||||
```
|
||||
|
||||
`typed_dict.py`:
|
||||
@@ -297,6 +303,33 @@ def _(flag: bool):
|
||||
reveal_type(x2) # revealed: list[int | None]
|
||||
```
|
||||
|
||||
## Dunder Calls
|
||||
|
||||
The key and value parameters types are used as type context for `__setitem__` dunder calls:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Bar(TypedDict):
|
||||
baz: float
|
||||
|
||||
def _(x: dict[str, Bar]):
|
||||
x["foo"] = reveal_type({"baz": 2}) # revealed: Bar
|
||||
|
||||
class X:
|
||||
def __setitem__(self, key: Bar, value: Bar): ...
|
||||
|
||||
def _(x: X):
|
||||
# revealed: Bar
|
||||
x[reveal_type({"baz": 1})] = reveal_type({"baz": 2}) # revealed: Bar
|
||||
|
||||
# TODO: Support type context with union subscripting.
|
||||
def _(x: X | dict[Bar, Bar]):
|
||||
# error: [invalid-assignment]
|
||||
# error: [invalid-assignment]
|
||||
x[{"baz": 1}] = {"baz": 2}
|
||||
```
|
||||
|
||||
## Multi-inference diagnostics
|
||||
|
||||
```toml
|
||||
|
||||
@@ -13,54 +13,6 @@ bool(1, 2)
|
||||
bool(NotBool())
|
||||
```
|
||||
|
||||
## Calls to `type()`
|
||||
|
||||
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
|
||||
tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
|
||||
tests for the `__class__` attribute.)
|
||||
|
||||
```py
|
||||
reveal_type(type(1)) # revealed: <class 'int'>
|
||||
```
|
||||
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
reveal_type(type("Foo", (), {})) # revealed: type
|
||||
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
|
||||
```
|
||||
|
||||
Other numbers of arguments are invalid
|
||||
|
||||
```py
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", ())
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
## Calls to `str()`
|
||||
|
||||
### Valid calls
|
||||
|
||||
@@ -464,10 +464,51 @@ reveal_type(C.f2(1)) # revealed: str
|
||||
reveal_type(C().f2(1)) # revealed: str
|
||||
```
|
||||
|
||||
### Classmethods with `Self` and callable-returning decorators
|
||||
|
||||
When a classmethod is decorated with a decorator that returns a callable type (like
|
||||
`@contextmanager`), `Self` in the return type should correctly resolve to the subclass when accessed
|
||||
on a derived class.
|
||||
|
||||
```py
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
from typing_extensions import Self
|
||||
|
||||
class Base:
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def create(cls) -> Iterator[Self]:
|
||||
yield cls()
|
||||
|
||||
class Child(Base): ...
|
||||
|
||||
reveal_type(Base.create()) # revealed: _GeneratorContextManager[Base, None, None]
|
||||
with Base.create() as base:
|
||||
reveal_type(base) # revealed: Base
|
||||
|
||||
reveal_type(Base().create()) # revealed: _GeneratorContextManager[Base, None, None]
|
||||
with Base().create() as base:
|
||||
reveal_type(base) # revealed: Base
|
||||
|
||||
reveal_type(Child.create()) # revealed: _GeneratorContextManager[Child, None, None]
|
||||
with Child.create() as child:
|
||||
reveal_type(child) # revealed: Child
|
||||
|
||||
reveal_type(Child().create()) # revealed: _GeneratorContextManager[Child, None, None]
|
||||
with Child().create() as child:
|
||||
reveal_type(child) # revealed: Child
|
||||
```
|
||||
|
||||
### `__init_subclass__`
|
||||
|
||||
The [`__init_subclass__`] method is implicitly a classmethod:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
@@ -480,6 +521,130 @@ class Derived(Base):
|
||||
reveal_type(Derived.custom_attribute) # revealed: int
|
||||
```
|
||||
|
||||
Subclasses must be constructed with arguments matching the required arguments of the base
|
||||
`__init_subclass__` method.
|
||||
|
||||
```py
|
||||
class Empty: ...
|
||||
|
||||
class RequiresArg:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class NoArg:
|
||||
def __init_subclass__(cls): ...
|
||||
|
||||
# Single-base definitions
|
||||
class MissingArg(RequiresArg): ... # error: [missing-argument]
|
||||
class InvalidType(RequiresArg, arg="foo"): ... # error: [invalid-argument-type]
|
||||
class Valid(RequiresArg, arg=1): ...
|
||||
|
||||
# error: [missing-argument]
|
||||
# error: [unknown-argument]
|
||||
class IncorrectArg(RequiresArg, not_arg="foo"): ...
|
||||
```
|
||||
|
||||
For multiple inheritance, the first resolved `__init_subclass__` method is used.
|
||||
|
||||
```py
|
||||
class Empty: ...
|
||||
|
||||
class RequiresArg:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class NoArg:
|
||||
def __init_subclass__(cls): ...
|
||||
|
||||
class Valid(NoArg, RequiresArg): ...
|
||||
class MissingArg(RequiresArg, NoArg): ... # error: [missing-argument]
|
||||
class InvalidType(RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type]
|
||||
class Valid(RequiresArg, NoArg, arg=1): ...
|
||||
|
||||
# Ensure base class without __init_subclass__ is ignored
|
||||
class Valid(Empty, NoArg): ...
|
||||
class Valid(Empty, RequiresArg, NoArg, arg=1): ...
|
||||
class MissingArg(Empty, RequiresArg): ... # error: [missing-argument]
|
||||
class MissingArg(Empty, RequiresArg, NoArg): ... # error: [missing-argument]
|
||||
class InvalidType(Empty, RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type]
|
||||
|
||||
# Multiple inheritance with args
|
||||
class Base(Empty, RequiresArg, NoArg, arg=1): ...
|
||||
class Valid(Base, arg=1): ...
|
||||
class MissingArg(Base): ... # error: [missing-argument]
|
||||
class InvalidType(Base, arg="foo"): ... # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
Keyword splats are allowed if their type can be determined:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class RequiresKwarg:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class WrongArg(TypedDict):
|
||||
kwarg: int
|
||||
|
||||
class InvalidType(TypedDict):
|
||||
arg: str
|
||||
|
||||
wrong_arg: WrongArg = {"kwarg": 5}
|
||||
|
||||
# error: [missing-argument]
|
||||
# error: [unknown-argument]
|
||||
class MissingArg(RequiresKwarg, **wrong_arg): ...
|
||||
|
||||
invalid_type: InvalidType = {"arg": "foo"}
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
class InvalidType(RequiresKwarg, **invalid_type): ...
|
||||
```
|
||||
|
||||
So are generics:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar, Literal, overload
|
||||
|
||||
class Base[T]:
|
||||
def __init_subclass__(cls, arg: T): ...
|
||||
|
||||
class Valid(Base[int], arg=1): ...
|
||||
class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type]
|
||||
|
||||
# Old generic syntax
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]):
|
||||
def __init_subclass__(cls, arg: T) -> None: ...
|
||||
|
||||
class Valid(Base[int], arg=1): ...
|
||||
class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
So are overloads:
|
||||
|
||||
```py
|
||||
class Base:
|
||||
@overload
|
||||
def __init_subclass__(cls, mode: Literal["a"], arg: int) -> None: ...
|
||||
@overload
|
||||
def __init_subclass__(cls, mode: Literal["b"], arg: str) -> None: ...
|
||||
def __init_subclass__(cls, mode: str, arg: int | str) -> None: ...
|
||||
|
||||
class Valid(Base, mode="a", arg=5): ...
|
||||
class Valid(Base, mode="b", arg="foo"): ...
|
||||
class InvalidType(Base, mode="b", arg=5): ... # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
The `metaclass` keyword is ignored, as it has special meaning and is not passed to
|
||||
`__init_subclass__` at runtime.
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class Valid(Base, arg=5, metaclass=object): ...
|
||||
```
|
||||
|
||||
## `@staticmethod`
|
||||
|
||||
### Basic
|
||||
@@ -621,17 +786,17 @@ argument:
|
||||
```py
|
||||
from typing_extensions import Self
|
||||
|
||||
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type(object.__new__) # revealed: def __new__[Self](cls) -> Self
|
||||
reveal_type(object().__new__) # revealed: def __new__[Self](cls) -> Self
|
||||
# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self]
|
||||
reveal_type(int.__new__)
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self]
|
||||
reveal_type((42).__new__)
|
||||
|
||||
class X:
|
||||
def __init__(self, val: int): ...
|
||||
def make_another(self) -> Self:
|
||||
reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(self.__new__) # revealed: def __new__[Self](cls) -> Self
|
||||
return self.__new__(type(self))
|
||||
```
|
||||
|
||||
|
||||
1089
crates/ty_python_semantic/resources/mdtest/call/type.md
Normal file
1089
crates/ty_python_semantic/resources/mdtest/call/type.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,3 +25,22 @@ B = bytes
|
||||
|
||||
reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'G[bytes]'>, typing.Generic, <class 'object'>)
|
||||
```
|
||||
|
||||
## Starred bases
|
||||
|
||||
These are currently not supported, but ideally we would support them in some limited situations.
|
||||
|
||||
```py
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
bases = (A, B, C)
|
||||
|
||||
class Foo(*bases): ...
|
||||
|
||||
# revealed: (<class 'Foo'>, @Todo(StarredExpression), <class 'object'>)
|
||||
reveal_mro(Foo)
|
||||
```
|
||||
|
||||
@@ -1032,4 +1032,125 @@ reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
## Calling decorator function directly with a class argument
|
||||
|
||||
When a function decorated with `@dataclass_transform()` is called directly with a class argument
|
||||
(not used as a decorator), it should return the class with the dataclass transformation applied.
|
||||
|
||||
### Basic case
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = my_dataclass(A)
|
||||
|
||||
reveal_type(B) # revealed: <class 'A'>
|
||||
|
||||
B(1)
|
||||
```
|
||||
|
||||
### Function with additional parameters
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T], *, order: bool = False) -> type[T]:
|
||||
return cls
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = my_dataclass(A, order=True)
|
||||
|
||||
reveal_type(B) # revealed: <class 'A'>
|
||||
|
||||
reveal_type(B(1) < B(2)) # revealed: bool
|
||||
```
|
||||
|
||||
### Overloaded decorator function
|
||||
|
||||
When the decorator function has overloads (one for direct class application, one for returning a
|
||||
decorator), calling it with a class should return the class type.
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Callable, overload
|
||||
|
||||
@overload
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]: ...
|
||||
@overload
|
||||
def my_dataclass[T]() -> Callable[[type[T]], type[T]]: ...
|
||||
def my_dataclass[T](cls: type[T] | None = None) -> type[T] | Callable[[type[T]], type[T]]:
|
||||
raise NotImplementedError
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = my_dataclass(A)
|
||||
|
||||
reveal_type(B) # revealed: <class 'A'>
|
||||
|
||||
B(1)
|
||||
```
|
||||
|
||||
### Passing a specialized generic class
|
||||
|
||||
When calling a `@dataclass_transform()` decorated function with a specialized generic class, the
|
||||
specialization should be preserved.
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
class A[T]:
|
||||
x: T
|
||||
|
||||
B = my_dataclass(A[int])
|
||||
|
||||
reveal_type(B) # revealed: <class 'A[int]'>
|
||||
|
||||
B(1)
|
||||
```
|
||||
|
||||
### Decorator factory with class parameter
|
||||
|
||||
When a `@dataclass_transform()` decorated function takes a class as a parameter but is used as a
|
||||
decorator factory (returns a decorator), the dataclass behavior should be applied to the decorated
|
||||
class, not to the parameter class.
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def hydrated_dataclass[T](target: type[T], *, frozen: bool = False):
|
||||
def decorator[U](cls: type[U]) -> type[U]:
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
class Target:
|
||||
pass
|
||||
|
||||
decorator = hydrated_dataclass(Target)
|
||||
reveal_type(decorator) # revealed: <decorator produced by dataclass-like function>
|
||||
|
||||
@hydrated_dataclass(Target)
|
||||
class Model:
|
||||
x: int
|
||||
|
||||
# Model should be a dataclass-like class with x as a field
|
||||
Model(x=1)
|
||||
reveal_type(Model.__init__) # revealed: (self: Model, x: int) -> None
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]:
|
||||
# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None
|
||||
reveal_type(ParentDataclass.__init__)
|
||||
|
||||
# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None
|
||||
# revealed: [T](self: ParentDataclass[T], value: T) -> None
|
||||
reveal_type(ChildOfParentDataclass.__init__)
|
||||
|
||||
result_int = uses_dataclass(42)
|
||||
@@ -1682,9 +1682,42 @@ def sequence4(cls: type) -> type:
|
||||
class Foo: ...
|
||||
|
||||
ordered_foo = dataclass(order=True)(Foo)
|
||||
reveal_type(ordered_foo) # revealed: type[Foo] & Any
|
||||
# TODO: should be `Foo & Any`
|
||||
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
|
||||
# TODO: should be `Any`
|
||||
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
|
||||
reveal_type(ordered_foo) # revealed: <class 'Foo'>
|
||||
reveal_type(ordered_foo()) # revealed: Foo
|
||||
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
|
||||
```
|
||||
|
||||
## Dynamic class literals
|
||||
|
||||
Dynamic classes created with `type()` can be wrapped with `dataclass()` as a function:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Basic dynamic class wrapped with dataclass
|
||||
DynamicFoo = type("DynamicFoo", (), {})
|
||||
DynamicFoo = dataclass(DynamicFoo)
|
||||
|
||||
# The class is recognized as a dataclass
|
||||
reveal_type(DynamicFoo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
# Can create instances
|
||||
instance = DynamicFoo()
|
||||
reveal_type(instance) # revealed: DynamicFoo
|
||||
```
|
||||
|
||||
Dynamic classes that inherit from a dataclass base also work:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: int
|
||||
|
||||
# Dynamic class inheriting from a dataclass
|
||||
DynamicChild = type("DynamicChild", (Base,), {})
|
||||
DynamicChild = dataclass(DynamicChild)
|
||||
|
||||
reveal_type(DynamicChild.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
```
|
||||
|
||||
@@ -38,6 +38,125 @@ reveal_type(s1 > s2) # revealed: bool
|
||||
reveal_type(s1 >= s2) # revealed: bool
|
||||
```
|
||||
|
||||
## Signature derived from source ordering method
|
||||
|
||||
When the source ordering method accepts a broader type (like `object`) for its `other` parameter,
|
||||
the synthesized comparison methods should use the same signature. This allows comparisons with types
|
||||
other than the class itself:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class Comparable:
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Comparable):
|
||||
return self.value == other.value
|
||||
if isinstance(other, int):
|
||||
return self.value == other
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
if isinstance(other, Comparable):
|
||||
return self.value < other.value
|
||||
if isinstance(other, int):
|
||||
return self.value < other
|
||||
return NotImplemented
|
||||
|
||||
a = Comparable(10)
|
||||
b = Comparable(20)
|
||||
|
||||
# Comparisons with the same type work.
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
# Comparisons with `int` also work because `__lt__` accepts `object`.
|
||||
reveal_type(a <= 15) # revealed: bool
|
||||
reveal_type(a >= 5) # revealed: bool
|
||||
```
|
||||
|
||||
## Multiple ordering methods with different signatures
|
||||
|
||||
When multiple ordering methods are defined with different signatures, the decorator selects a "root"
|
||||
method using the priority order: `__lt__` > `__le__` > `__gt__` > `__ge__`. Synthesized methods use
|
||||
the signature from the highest-priority method. Methods that are explicitly defined are not
|
||||
overridden.
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class MultiSig:
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
# __lt__ accepts `object` (highest priority, used as root)
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return True
|
||||
# __gt__ only accepts `MultiSig` (not overridden by decorator)
|
||||
def __gt__(self, other: "MultiSig") -> bool:
|
||||
return True
|
||||
|
||||
a = MultiSig(10)
|
||||
b = MultiSig(20)
|
||||
|
||||
# __le__ and __ge__ are synthesized with __lt__'s signature (accepts `object`)
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a <= 15) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
reveal_type(a >= 15) # revealed: bool
|
||||
|
||||
# __gt__ keeps its original signature (only accepts MultiSig)
|
||||
reveal_type(a > b) # revealed: bool
|
||||
a > 15 # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
## Overloaded ordering method
|
||||
|
||||
When the source ordering method is overloaded, the synthesized comparison methods should preserve
|
||||
all overloads:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
from typing import overload
|
||||
|
||||
@total_ordering
|
||||
class Flexible:
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
@overload
|
||||
def __lt__(self, other: "Flexible") -> bool: ...
|
||||
@overload
|
||||
def __lt__(self, other: int) -> bool: ...
|
||||
def __lt__(self, other: "Flexible | int") -> bool:
|
||||
if isinstance(other, Flexible):
|
||||
return self.value < other.value
|
||||
return self.value < other
|
||||
|
||||
a = Flexible(10)
|
||||
b = Flexible(20)
|
||||
|
||||
# Synthesized __le__ preserves overloads from __lt__
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a <= 15) # revealed: bool
|
||||
|
||||
# Synthesized __ge__ also preserves overloads
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
reveal_type(a >= 15) # revealed: bool
|
||||
|
||||
# But comparison with an unsupported type should still error
|
||||
a <= "string" # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
## Using `__gt__` as the root comparison method
|
||||
|
||||
When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, `__le__`, and
|
||||
@@ -127,6 +246,41 @@ reveal_type(c1 > c2) # revealed: bool
|
||||
reveal_type(c1 >= c2) # revealed: bool
|
||||
```
|
||||
|
||||
## Method precedence with inheritance
|
||||
|
||||
The decorator always prefers `__lt__` > `__le__` > `__gt__` > `__ge__`, regardless of whether the
|
||||
method is defined locally or inherited. In this example, the inherited `__lt__` takes precedence
|
||||
over the locally-defined `__gt__`:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
from typing import Literal
|
||||
|
||||
class Base:
|
||||
def __lt__(self, other: "Base") -> Literal[True]:
|
||||
return True
|
||||
|
||||
@total_ordering
|
||||
class Child(Base):
|
||||
# __gt__ is defined locally, but __lt__ (inherited) takes precedence
|
||||
def __gt__(self, other: "Child") -> Literal[False]:
|
||||
return False
|
||||
|
||||
c1 = Child()
|
||||
c2 = Child()
|
||||
|
||||
# __lt__ is inherited from Base
|
||||
reveal_type(c1 < c2) # revealed: Literal[True]
|
||||
|
||||
# __gt__ is defined locally on Child
|
||||
reveal_type(c1 > c2) # revealed: Literal[False]
|
||||
|
||||
# __le__ and __ge__ are synthesized from __lt__ (the highest-priority method),
|
||||
# even though __gt__ is defined locally on the class itself
|
||||
reveal_type(c1 <= c2) # revealed: bool
|
||||
reveal_type(c1 >= c2) # revealed: bool
|
||||
```
|
||||
|
||||
## Explicitly-defined methods are not overridden
|
||||
|
||||
When a class explicitly defines multiple comparison methods, the decorator does not override them.
|
||||
@@ -244,3 +398,200 @@ reveal_type(n1 > n2) # revealed: bool
|
||||
n1 <= n2 # error: [unsupported-operator]
|
||||
n1 >= n2 # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
## Non-bool return type
|
||||
|
||||
When the root ordering method returns a non-bool type (like `int`), the synthesized methods return a
|
||||
union of that type and `bool`. This is because `@total_ordering` generates methods like:
|
||||
|
||||
```python
|
||||
def __le__(self, other):
|
||||
return self < other or self == other
|
||||
```
|
||||
|
||||
If `__lt__` returns `int`, then the synthesized `__le__` could return either `int` (from
|
||||
`self < other`) or `bool` (from `self == other`). Since `bool` is a subtype of `int`, the union
|
||||
simplifies to `int`:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class IntReturn:
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, IntReturn):
|
||||
return NotImplemented
|
||||
return self.value == other.value
|
||||
|
||||
def __lt__(self, other: "IntReturn") -> int:
|
||||
return self.value - other.value
|
||||
|
||||
a = IntReturn(10)
|
||||
b = IntReturn(20)
|
||||
|
||||
# User-defined __lt__ returns int.
|
||||
reveal_type(a < b) # revealed: int
|
||||
|
||||
# Synthesized methods return int (the union int | bool simplifies to int
|
||||
# because bool is a subtype of int in Python).
|
||||
reveal_type(a <= b) # revealed: int
|
||||
reveal_type(a > b) # revealed: int
|
||||
reveal_type(a >= b) # revealed: int
|
||||
```
|
||||
|
||||
When the root method returns a type that is not a supertype of `bool`, the union is preserved:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class StrReturn:
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, StrReturn):
|
||||
return NotImplemented
|
||||
return self.value == other.value
|
||||
|
||||
def __lt__(self, other: "StrReturn") -> str:
|
||||
return self.value
|
||||
|
||||
a = StrReturn("a")
|
||||
b = StrReturn("b")
|
||||
|
||||
# User-defined __lt__ returns str.
|
||||
reveal_type(a < b) # revealed: str
|
||||
|
||||
# Synthesized methods return str | bool.
|
||||
reveal_type(a <= b) # revealed: str | bool
|
||||
reveal_type(a > b) # revealed: str | bool
|
||||
reveal_type(a >= b) # revealed: str | bool
|
||||
```
|
||||
|
||||
## Function call form
|
||||
|
||||
When `total_ordering` is called as a function (not as a decorator), the same validation is
|
||||
performed:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
class NoOrderingMethod:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
# error: [invalid-total-ordering]
|
||||
InvalidOrderedClass = total_ordering(NoOrderingMethod)
|
||||
```
|
||||
|
||||
When the class does define an ordering method, no error is emitted:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
class HasOrderingMethod:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __lt__(self, other: "HasOrderingMethod") -> bool:
|
||||
return True
|
||||
|
||||
# No error (class defines `__lt__`).
|
||||
ValidOrderedClass = total_ordering(HasOrderingMethod)
|
||||
reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]
|
||||
```
|
||||
|
||||
## Function call form with `type()`
|
||||
|
||||
When `total_ordering` is called on a class created with `type()`, the same validation is performed:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
def lt_impl(self, other) -> bool:
|
||||
return True
|
||||
|
||||
# No error: the functional class defines `__lt__` in its namespace
|
||||
ValidFunctional = total_ordering(type("ValidFunctional", (), {"__lt__": lt_impl}))
|
||||
|
||||
InvalidFunctionalBase = type("InvalidFunctionalBase", (), {})
|
||||
# error: [invalid-total-ordering]
|
||||
InvalidFunctional = total_ordering(InvalidFunctionalBase)
|
||||
```
|
||||
|
||||
## Inherited from functional class
|
||||
|
||||
When a class inherits from a functional class that defines an ordering method, `@total_ordering`
|
||||
correctly detects it:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
def lt_impl(self, other) -> bool:
|
||||
return True
|
||||
|
||||
def eq_impl(self, other) -> bool:
|
||||
return True
|
||||
|
||||
# Functional class with __lt__ method
|
||||
OrderedBase = type("OrderedBase", (), {"__lt__": lt_impl})
|
||||
|
||||
# A class inheriting from OrderedBase gets the ordering method
|
||||
@total_ordering
|
||||
class Ordered(OrderedBase):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
o1 = Ordered()
|
||||
o2 = Ordered()
|
||||
|
||||
# Inherited __lt__ is available
|
||||
reveal_type(o1 < o2) # revealed: bool
|
||||
|
||||
# @total_ordering synthesizes the other methods
|
||||
reveal_type(o1 <= o2) # revealed: bool
|
||||
reveal_type(o1 > o2) # revealed: bool
|
||||
reveal_type(o1 >= o2) # revealed: bool
|
||||
```
|
||||
|
||||
When the dynamic base class does not define any ordering method, `@total_ordering` emits an error:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
# Dynamic class without ordering methods (invalid for @total_ordering)
|
||||
NoOrderBase = type("NoOrderBase", (), {})
|
||||
|
||||
@total_ordering # error: [invalid-total-ordering]
|
||||
class NoOrder(NoOrderBase):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
```
|
||||
|
||||
## Dynamic namespace
|
||||
|
||||
When a `type()`-constructed class has a dynamic namespace, we assume it might provide an ordering
|
||||
method (since we can't know what's in the namespace). No error is emitted when such a class is
|
||||
passed to `@total_ordering`:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
from typing import Any
|
||||
|
||||
def f(ns: dict[str, Any]):
|
||||
# Dynamic class with dynamic namespace - might have ordering methods
|
||||
DynamicBase = type("DynamicBase", (), ns)
|
||||
|
||||
# No error: the dynamic namespace might contain __lt__ or another ordering method
|
||||
@total_ordering
|
||||
class Ordered(DynamicBase):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
# Also works when calling total_ordering as a function
|
||||
OrderedDirect = total_ordering(type("OrderedDirect", (), ns))
|
||||
```
|
||||
|
||||
@@ -86,7 +86,7 @@ class MyClass: ...
|
||||
def get_MyClass() -> MyClass:
|
||||
from . import make_MyClass
|
||||
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1`, found `package.foo.MyClass @ src/package/foo.pyi:1`"
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1:7`, found `package.foo.MyClass @ src/package/foo.pyi:1:7`"
|
||||
return make_MyClass()
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Unsupported base for dynamic `type()` classes
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## `@final` class
|
||||
|
||||
Classes decorated with `@final` cannot be subclassed:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class FinalClass:
|
||||
pass
|
||||
|
||||
X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
|
||||
```
|
||||
|
||||
## `Generic` base
|
||||
|
||||
Dynamic classes created via `type()` cannot inherit from `Generic`:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
X = type("X", (Generic[T],), {}) # error: [invalid-base]
|
||||
```
|
||||
|
||||
## `Protocol` base
|
||||
|
||||
Dynamic classes created via `type()` cannot inherit from `Protocol`:
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
|
||||
X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
|
||||
```
|
||||
|
||||
## `TypedDict` base
|
||||
|
||||
Dynamic classes created via `type()` cannot inherit from `TypedDict` directly. Use
|
||||
`TypedDict("Name", ...)` instead:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
X = type("X", (TypedDict,), {}) # error: [invalid-base]
|
||||
```
|
||||
|
||||
## Enum base
|
||||
|
||||
Dynamic classes created via `type()` cannot inherit from Enum classes because `EnumMeta` expects
|
||||
special dict attributes that `type()` doesn't provide:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class MyEnum(Enum):
|
||||
pass
|
||||
|
||||
X = type("X", (MyEnum,), {}) # error: [invalid-base]
|
||||
```
|
||||
|
||||
## Enum with members
|
||||
|
||||
Enums with members are final and cannot be subclassed at all:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
|
||||
X = type("X", (Color,), {}) # error: [subclass-of-final-class]
|
||||
```
|
||||
@@ -1016,6 +1016,108 @@ class Color(Enum):
|
||||
reveal_type(Color.RED != Color.RED) # revealed: bool
|
||||
```
|
||||
|
||||
## Generic enums are invalid
|
||||
|
||||
Enum classes cannot be generic. Python does not support generic enums, and attempting to create one
|
||||
will result in a `TypeError` at runtime.
|
||||
|
||||
### PEP 695 syntax
|
||||
|
||||
Using PEP 695 type parameters on an enum is invalid:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
# error: [invalid-generic-enum] "Enum class `E` cannot be generic"
|
||||
class E[T](Enum):
|
||||
A = 1
|
||||
B = 2
|
||||
```
|
||||
|
||||
### Legacy `Generic` base class
|
||||
|
||||
Inheriting from both `Enum` and `Generic[T]` is also invalid:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-generic-enum] "Enum class `F` cannot be generic"
|
||||
class F(Enum, Generic[T]):
|
||||
A = 1
|
||||
B = 2
|
||||
```
|
||||
|
||||
### Swapped order (`Generic` first)
|
||||
|
||||
The order of bases doesn't matter; it's still invalid:
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-generic-enum] "Enum class `G` cannot be generic"
|
||||
class G(Generic[T], Enum):
|
||||
A = 1
|
||||
B = 2
|
||||
```
|
||||
|
||||
### Enum subclasses
|
||||
|
||||
Subclasses of enum base classes also cannot be generic:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from enum import Enum, IntEnum
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic"
|
||||
class MyIntEnum[T](IntEnum):
|
||||
A = 1
|
||||
|
||||
# error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic"
|
||||
class MyFlagEnum(IntEnum, Generic[T]):
|
||||
A = 1
|
||||
```
|
||||
|
||||
### Custom enum base class
|
||||
|
||||
Even with custom enum subclasses that don't have members, they cannot be made generic:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class MyEnumBase(Enum):
|
||||
def some_method(self) -> None: ...
|
||||
|
||||
# error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic"
|
||||
class MyEnum[T](MyEnumBase):
|
||||
A = 1
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>
|
||||
|
||||
@@ -43,13 +43,11 @@ reveal_type(len((1,))) # revealed: Literal[1]
|
||||
reveal_type(len((1, 2))) # revealed: Literal[2]
|
||||
reveal_type(len(tuple())) # revealed: Literal[0]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
reveal_type(len((*[],))) # revealed: Literal[0]
|
||||
|
||||
# fmt: off
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[1]
|
||||
reveal_type(len( # revealed: Literal[2]
|
||||
reveal_type(len( # revealed: Literal[1]
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
@@ -58,11 +56,8 @@ reveal_type(len( # revealed: Literal[2]
|
||||
|
||||
# fmt: on
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[2]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
Tuple subclasses:
|
||||
|
||||
@@ -817,7 +817,7 @@ class WithOverloadedMethod(Generic[T]):
|
||||
def method(self, x: S | T) -> S | T:
|
||||
return x
|
||||
|
||||
# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
|
||||
# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int]
|
||||
reveal_type(WithOverloadedMethod[int].method)
|
||||
```
|
||||
|
||||
|
||||
@@ -171,6 +171,27 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Bounded typevars in contravariant positions
|
||||
|
||||
When a bounded typevar appears in a contravariant position, the actual type doesn't need to satisfy
|
||||
the bound directly. The typevar can be solved to the intersection of the actual type and the bound
|
||||
(e.g., `Never` when disjoint).
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T", contravariant=True)
|
||||
T_int = TypeVar("T_int", bound=int)
|
||||
|
||||
class Contra(Generic[T]): ...
|
||||
|
||||
def f(x: Contra[T_int]) -> T_int:
|
||||
raise NotImplementedError
|
||||
|
||||
def _(x: Contra[str]):
|
||||
reveal_type(f(x)) # revealed: Never
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
||||
With an invariant typevar, only equivalent specializations of the generic class are subtypes of or
|
||||
|
||||
@@ -702,7 +702,7 @@ class WithOverloadedMethod[T]:
|
||||
def method[S](self, x: S | T) -> S | T:
|
||||
return x
|
||||
|
||||
# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
|
||||
# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int]
|
||||
reveal_type(WithOverloadedMethod[int].method)
|
||||
```
|
||||
|
||||
|
||||
@@ -867,7 +867,7 @@ class ClassWithOverloadedInit[T]:
|
||||
# overload. We would then also have to determine that R must be equal to the return type of **P's
|
||||
# solution.
|
||||
|
||||
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
|
||||
# revealed: Overload[[T](x: int) -> ClassWithOverloadedInit[int], [T](x: str) -> ClassWithOverloadedInit[str]]
|
||||
reveal_type(into_callable(ClassWithOverloadedInit))
|
||||
# TODO: revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
|
||||
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str], (x: str) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]]
|
||||
@@ -888,7 +888,7 @@ class GenericClass[T]:
|
||||
def _(x: list[str]):
|
||||
# TODO: This fails because we are not propagating GenericClass's generic context into the
|
||||
# Callable that we create for it.
|
||||
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
|
||||
# revealed: [T](x: list[T], y: list[T]) -> GenericClass[T]
|
||||
reveal_type(into_callable(GenericClass))
|
||||
# revealed: ty_extensions.GenericContext[T@GenericClass]
|
||||
reveal_type(generic_context(into_callable(GenericClass)))
|
||||
|
||||
@@ -172,6 +172,23 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
|
||||
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
|
||||
```
|
||||
|
||||
## Bounded typevars in contravariant positions
|
||||
|
||||
When a bounded typevar appears in a contravariant position, the actual type doesn't need to satisfy
|
||||
the bound directly. The typevar can be solved to the intersection of the actual type and the bound
|
||||
(e.g., `Never` when disjoint).
|
||||
|
||||
```py
|
||||
class Contra[T]:
|
||||
def append(self, x: T): ...
|
||||
|
||||
def f[T: int](x: Contra[T]) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
def _(x: Contra[str]):
|
||||
reveal_type(f(x)) # revealed: Never
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
||||
With an invariant typevar, only equivalent specializations of the generic class are subtypes of or
|
||||
@@ -780,7 +797,7 @@ class B(A):
|
||||
pass
|
||||
|
||||
class C[T]:
|
||||
def check(x: object) -> TypeIs[T]:
|
||||
def check(self, x: object) -> TypeIs[T]:
|
||||
# this is a bad check, but we only care about it type-checking
|
||||
return False
|
||||
|
||||
@@ -818,7 +835,7 @@ class B(A):
|
||||
pass
|
||||
|
||||
class C[T]:
|
||||
def check(x: object) -> TypeGuard[T]:
|
||||
def check(self, x: object) -> TypeGuard[T]:
|
||||
# this is a bad check, but we only care about it type-checking
|
||||
return False
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ already solved and specialized when the class was specialized:
|
||||
from ty_extensions import generic_context
|
||||
|
||||
legacy.m("string", None) # error: [invalid-argument-type]
|
||||
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m
|
||||
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S) -> S
|
||||
# revealed: ty_extensions.GenericContext[T@Legacy]
|
||||
reveal_type(generic_context(Legacy))
|
||||
# revealed: ty_extensions.GenericContext[Self@m, S@m]
|
||||
@@ -344,4 +344,27 @@ class C[T]:
|
||||
ok2: Inner[T]
|
||||
```
|
||||
|
||||
## Mixed-scope type parameters
|
||||
|
||||
Methods can have type parameters that are scoped to the method itself, while also referring to type
|
||||
parameters from the enclosing class.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from ty_extensions import into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
class Foo(Generic[T]):
|
||||
def bar(self, x: T, y: S) -> tuple[T, S]:
|
||||
raise NotImplementedError
|
||||
|
||||
def f(x: type[Foo[T]]) -> T:
|
||||
# revealed: [S](self, x: T@f, y: S) -> tuple[T@f, S]
|
||||
reveal_type(into_callable(x.bar))
|
||||
raise NotImplementedError
|
||||
```
|
||||
|
||||
[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables
|
||||
|
||||
@@ -870,6 +870,102 @@ static_assert(not has_member(F, "__match_args__"))
|
||||
static_assert(not has_member(F(), "__weakref__"))
|
||||
```
|
||||
|
||||
### Dynamic classes (created via `type()`)
|
||||
|
||||
Dynamic classes created using the three-argument form of `type()` support autocomplete for members
|
||||
inherited from their base classes on the class object:
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
class Base:
|
||||
base_attr: int = 1
|
||||
|
||||
def base_method(self) -> str:
|
||||
return "hello"
|
||||
|
||||
class Mixin:
|
||||
mixin_attr: str = "mixin"
|
||||
|
||||
# Dynamic class with a single base
|
||||
DynamicSingle = type("DynamicSingle", (Base,), {})
|
||||
|
||||
# The class object has access to base class attributes
|
||||
static_assert(has_member(DynamicSingle, "base_attr"))
|
||||
static_assert(has_member(DynamicSingle, "base_method"))
|
||||
|
||||
# Dynamic class with multiple bases
|
||||
DynamicMulti = type("DynamicMulti", (Base, Mixin), {})
|
||||
|
||||
static_assert(has_member(DynamicMulti, "base_attr"))
|
||||
static_assert(has_member(DynamicMulti, "mixin_attr"))
|
||||
```
|
||||
|
||||
Members from `object` and the `type` metaclass are available on the class object:
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
Dynamic = type("Dynamic", (), {})
|
||||
|
||||
# object members are available on the class
|
||||
static_assert(has_member(Dynamic, "__doc__"))
|
||||
static_assert(has_member(Dynamic, "__init__"))
|
||||
|
||||
# type metaclass members are available on the class
|
||||
static_assert(has_member(Dynamic, "__name__"))
|
||||
static_assert(has_member(Dynamic, "__bases__"))
|
||||
static_assert(has_member(Dynamic, "__mro__"))
|
||||
static_assert(has_member(Dynamic, "__subclasses__"))
|
||||
```
|
||||
|
||||
Attributes from the namespace dict (third argument) are not tracked:
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
DynamicWithDict = type("DynamicWithDict", (), {"custom_attr": 42})
|
||||
|
||||
# TODO: these should pass -- namespace dict attributes are not yet available for autocomplete
|
||||
static_assert(has_member(DynamicWithDict, "custom_attr")) # error: [static-assert-error]
|
||||
static_assert(has_member(DynamicWithDict(), "custom_attr")) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
Dynamic classes inheriting from classes with custom metaclasses get metaclass members:
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
class MyMeta(type):
|
||||
meta_attr: str = "meta"
|
||||
|
||||
class Base(metaclass=MyMeta):
|
||||
base_attr: int = 1
|
||||
|
||||
Dynamic = type("Dynamic", (Base,), {})
|
||||
|
||||
# Metaclass attributes are available on the class
|
||||
static_assert(has_member(Dynamic, "meta_attr"))
|
||||
static_assert(has_member(Dynamic, "base_attr"))
|
||||
```
|
||||
|
||||
However, instances of dynamic classes currently do not expose members for autocomplete:
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
class Base:
|
||||
base_attr: int = 1
|
||||
|
||||
DynamicSingle = type("DynamicSingle", (Base,), {})
|
||||
instance = DynamicSingle()
|
||||
|
||||
# TODO: these should pass; instance members should be available
|
||||
static_assert(has_member(instance, "base_attr")) # error: [static-assert-error]
|
||||
static_assert(has_member(instance, "__repr__")) # error: [static-assert-error]
|
||||
static_assert(has_member(instance, "__hash__")) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
### Attributes not available at runtime
|
||||
|
||||
Typeshed includes some attributes in `object` that are not available for some (builtin) types. For
|
||||
|
||||
@@ -140,7 +140,11 @@ If a child class's method definition is Liskov-compatible with the method defini
|
||||
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
|
||||
on its grandparent class. This is because type checkers will treat the child class as a subtype of
|
||||
the grandparent class just as much as they treat it as a subtype of the parent class, so
|
||||
substitutability with respect to the grandparent class is just as important:
|
||||
substitutability with respect to the grandparent class is just as important.
|
||||
|
||||
However, if the parent class itself already has an LSP violation with an ancestor, we do not report
|
||||
the same violation for the child class. This is because the child class cannot fix the violation
|
||||
without introducing a new, worse violation against its immediate parent's contract.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
@@ -156,13 +160,31 @@ class Parent(Grandparent):
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class Child(Parent):
|
||||
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`.
|
||||
# However, since `Parent.method` already violates LSP with `Grandparent.method`,
|
||||
# we don't report the same violation for `Child` -- it's inherited from `Parent`.
|
||||
def method(self, x: str) -> None: ...
|
||||
|
||||
class OtherChild(Parent):
|
||||
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class ChildWithNewViolation(Parent):
|
||||
# incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
|
||||
# We report the violation against the immediate parent (`Parent`), not the grandparent.
|
||||
def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class GrandparentWithReturnType:
|
||||
def method(self) -> int: ...
|
||||
|
||||
class ParentWithReturnType(GrandparentWithReturnType):
|
||||
def method(self) -> str: ... # error: [invalid-method-override]
|
||||
|
||||
class ChildWithReturnType(ParentWithReturnType):
|
||||
# Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
|
||||
# but not with `ParentWithReturnType.method`. We report against the immediate parent.
|
||||
def method(self) -> int: ... # error: [invalid-method-override]
|
||||
|
||||
class GradualParent(Grandparent):
|
||||
def method(self, x: Any) -> None: ...
|
||||
|
||||
@@ -190,8 +212,9 @@ class C(B):
|
||||
foo = get
|
||||
|
||||
class D(C):
|
||||
# compatible with `C.get` and `B.get`, but not with `A.get`
|
||||
def get(self, my_default): ... # error: [invalid-method-override]
|
||||
# compatible with `C.get` and `B.get`, but not with `A.get`.
|
||||
# Since `B.get` already violates LSP with `A.get`, we don't report for `D`.
|
||||
def get(self, my_default): ...
|
||||
```
|
||||
|
||||
## Non-generic methods on generic classes work as expected
|
||||
|
||||
@@ -398,10 +398,10 @@ if returns_bool():
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
# revealed: (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
reveal_mro(B)
|
||||
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)
|
||||
|
||||
@@ -84,17 +84,490 @@ alice.id = 42
|
||||
bob.age = None
|
||||
```
|
||||
|
||||
Alternative functional syntax:
|
||||
Alternative functional syntax with a list of tuples:
|
||||
|
||||
```py
|
||||
Person2 = NamedTuple("Person", [("id", int), ("name", str)])
|
||||
alice2 = Person2(1, "Alice")
|
||||
|
||||
# TODO: should be an error
|
||||
# error: [missing-argument]
|
||||
Person2(1)
|
||||
|
||||
reveal_type(alice2.id) # revealed: @Todo(functional `NamedTuple` syntax)
|
||||
reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax)
|
||||
reveal_type(alice2.id) # revealed: int
|
||||
reveal_type(alice2.name) # revealed: str
|
||||
```
|
||||
|
||||
Functional syntax with a tuple of tuples:
|
||||
|
||||
```py
|
||||
Person3 = NamedTuple("Person", (("id", int), ("name", str)))
|
||||
alice3 = Person3(1, "Alice")
|
||||
|
||||
reveal_type(alice3.id) # revealed: int
|
||||
reveal_type(alice3.name) # revealed: str
|
||||
```
|
||||
|
||||
Functional syntax with a tuple of lists:
|
||||
|
||||
```py
|
||||
Person4 = NamedTuple("Person", (["id", int], ["name", str]))
|
||||
alice4 = Person4(1, "Alice")
|
||||
|
||||
reveal_type(alice4.id) # revealed: int
|
||||
reveal_type(alice4.name) # revealed: str
|
||||
```
|
||||
|
||||
Functional syntax with a list of lists:
|
||||
|
||||
```py
|
||||
Person5 = NamedTuple("Person", [["id", int], ["name", str]])
|
||||
alice5 = Person5(1, "Alice")
|
||||
|
||||
reveal_type(alice5.id) # revealed: int
|
||||
reveal_type(alice5.name) # revealed: str
|
||||
```
|
||||
|
||||
### Functional syntax with variable name
|
||||
|
||||
When the typename is passed via a variable, we can extract it from the inferred literal string type:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
name = "Person"
|
||||
Person = NamedTuple(name, [("id", int), ("name", str)])
|
||||
|
||||
p = Person(1, "Alice")
|
||||
reveal_type(p.id) # revealed: int
|
||||
reveal_type(p.name) # revealed: str
|
||||
```
|
||||
|
||||
### Functional syntax with tuple variable fields
|
||||
|
||||
When fields are passed via a tuple variable, we can extract the literal field names and types from
|
||||
the inferred tuple type:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from ty_extensions import static_assert, is_subtype_of, reveal_mro
|
||||
|
||||
fields = (("host", str), ("port", int))
|
||||
Url = NamedTuple("Url", fields)
|
||||
|
||||
url = Url("localhost", 8080)
|
||||
reveal_type(url.host) # revealed: str
|
||||
reveal_type(url.port) # revealed: int
|
||||
|
||||
# Generic types are also correctly converted to instance types.
|
||||
generic_fields = (("items", list[int]), ("mapping", dict[str, bool]))
|
||||
Container = NamedTuple("Container", generic_fields)
|
||||
container = Container([1, 2, 3], {"a": True})
|
||||
reveal_type(container.items) # revealed: list[int]
|
||||
reveal_type(container.mapping) # revealed: dict[str, bool]
|
||||
|
||||
# MRO includes the properly specialized tuple type.
|
||||
# revealed: (<class 'Url'>, <class 'tuple[str, int]'>, <class 'object'>)
|
||||
reveal_mro(Url)
|
||||
|
||||
static_assert(is_subtype_of(Url, tuple[str, int]))
|
||||
|
||||
# Invalid type expressions in fields produce a diagnostic.
|
||||
invalid_fields = (("x", 42),) # 42 is not a valid type
|
||||
# error: [invalid-type-form] "Invalid type `Literal[42]` in `NamedTuple` field type"
|
||||
InvalidNT = NamedTuple("InvalidNT", invalid_fields)
|
||||
reveal_type(InvalidNT) # revealed: <class 'InvalidNT'>
|
||||
|
||||
# Unpacking works correctly with the field types.
|
||||
host, port = url
|
||||
reveal_type(host) # revealed: str
|
||||
reveal_type(port) # revealed: int
|
||||
|
||||
# error: [invalid-assignment] "Too many values to unpack: Expected 1"
|
||||
(only_one,) = url
|
||||
|
||||
# error: [invalid-assignment] "Not enough values to unpack: Expected 3"
|
||||
a, b, c = url
|
||||
|
||||
# Indexing works correctly.
|
||||
reveal_type(url[0]) # revealed: str
|
||||
reveal_type(url[1]) # revealed: int
|
||||
|
||||
# error: [index-out-of-bounds]
|
||||
url[2]
|
||||
```
|
||||
|
||||
### Functional syntax with variadic tuple fields
|
||||
|
||||
When fields are passed as a variadic tuple (e.g., `tuple[..., *tuple[T, ...]]`), we cannot determine
|
||||
the exact field count statically. In this case, we fall back to unknown fields:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
# Variadic tuple - we can't determine the exact fields statically.
|
||||
def get_fields() -> tuple[tuple[str, type[int]], *tuple[tuple[str, type[str]], ...]]:
|
||||
return (("x", int), ("y", str))
|
||||
|
||||
fields = get_fields()
|
||||
NT = NamedTuple("NT", fields)
|
||||
|
||||
# Fields are unknown, so attribute access returns Any and MRO has Unknown tuple.
|
||||
reveal_type(NT) # revealed: <class 'NT'>
|
||||
reveal_mro(NT) # revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
|
||||
reveal_type(NT(1, "a").x) # revealed: Any
|
||||
```
|
||||
|
||||
Similarly for `collections.namedtuple`:
|
||||
|
||||
```py
|
||||
import collections
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
def get_field_names() -> tuple[str, *tuple[str, ...]]:
|
||||
return ("x", "y")
|
||||
|
||||
field_names = get_field_names()
|
||||
NT = collections.namedtuple("NT", field_names)
|
||||
|
||||
# Fields are unknown, so attribute access returns Any and MRO has Unknown tuple.
|
||||
reveal_type(NT) # revealed: <class 'NT'>
|
||||
reveal_mro(NT) # revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
|
||||
reveal_type(NT(1, 2).x) # revealed: Any
|
||||
```
|
||||
|
||||
### Class inheriting from functional NamedTuple
|
||||
|
||||
Classes can inherit from functional namedtuples. The constructor parameters and field types are
|
||||
properly inherited:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
class Url(NamedTuple("Url", [("host", str), ("path", str)])):
|
||||
pass
|
||||
|
||||
reveal_type(Url) # revealed: <class 'Url'>
|
||||
# revealed: (<class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:7'>, <class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:11'>, <class 'tuple[str, str]'>, <class 'object'>)
|
||||
reveal_mro(Url)
|
||||
reveal_type(Url.__new__) # revealed: (cls: type, host: str, path: str) -> Url
|
||||
|
||||
# Constructor works with the inherited fields.
|
||||
url = Url("example.com", "/path")
|
||||
reveal_type(url) # revealed: Url
|
||||
reveal_type(url.host) # revealed: str
|
||||
reveal_type(url.path) # revealed: str
|
||||
|
||||
# Error handling works correctly.
|
||||
# error: [missing-argument]
|
||||
Url("example.com")
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
Url("example.com", "/path", "extra")
|
||||
```
|
||||
|
||||
Subclasses can add methods that use inherited fields:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from typing_extensions import Self
|
||||
|
||||
class Url(NamedTuple("Url", [("host", str), ("port", int)])):
|
||||
def with_port(self, port: int) -> Self:
|
||||
reveal_type(self.host) # revealed: str
|
||||
reveal_type(self.port) # revealed: int
|
||||
return self._replace(port=port)
|
||||
|
||||
url = Url("localhost", 8080)
|
||||
reveal_type(url.with_port(9000)) # revealed: Url
|
||||
```
|
||||
|
||||
For `class Foo(namedtuple("Foo", ...)): ...`, the inner call creates a namedtuple class, but the
|
||||
outer class is just a regular class inheriting from it. This is equivalent to:
|
||||
|
||||
```py
|
||||
class _Foo(NamedTuple): ...
|
||||
|
||||
class Foo(_Foo): # Regular class, not a namedtuple
|
||||
...
|
||||
```
|
||||
|
||||
Because the outer class is not itself a namedtuple, it can use `super()` and override `__new__`:
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
from typing import NamedTuple
|
||||
|
||||
class ExtType(namedtuple("ExtType", "code data")):
|
||||
"""Override __new__ to add validation."""
|
||||
|
||||
def __new__(cls, code, data):
|
||||
if not isinstance(code, int):
|
||||
raise TypeError("code must be int")
|
||||
return super().__new__(cls, code, data)
|
||||
|
||||
class Url(NamedTuple("Url", [("host", str), ("path", str)])):
|
||||
"""Override __new__ to normalize the path."""
|
||||
|
||||
def __new__(cls, host, path):
|
||||
if path and not path.startswith("/"):
|
||||
path = "/" + path
|
||||
return super().__new__(cls, host, path)
|
||||
|
||||
# Both work correctly.
|
||||
ext = ExtType(42, b"hello")
|
||||
reveal_type(ext) # revealed: ExtType
|
||||
|
||||
url = Url("example.com", "path")
|
||||
reveal_type(url) # revealed: Url
|
||||
```
|
||||
|
||||
### Functional syntax with list variable fields
|
||||
|
||||
When fields are passed via a list variable (not a literal), the field names cannot be determined
|
||||
statically. Attribute access returns `Any` and the constructor accepts any arguments:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from typing_extensions import Self
|
||||
|
||||
fields = [("host", str), ("port", int)]
|
||||
|
||||
class Url(NamedTuple("Url", fields)):
|
||||
def with_port(self, port: int) -> Self:
|
||||
# Fields are unknown, so attribute access returns Any.
|
||||
reveal_type(self.host) # revealed: Any
|
||||
reveal_type(self.port) # revealed: Any
|
||||
reveal_type(self.unknown) # revealed: Any
|
||||
return self._replace(port=port)
|
||||
```
|
||||
|
||||
When constructing a namedtuple directly with dynamically-defined fields, keyword arguments are
|
||||
accepted because the constructor uses a gradual signature:
|
||||
|
||||
```py
|
||||
import collections
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
CheckerConfig = ["duration", "video_fps", "audio_sample_rate"]
|
||||
GroundTruth = collections.namedtuple("GroundTruth", " ".join(CheckerConfig))
|
||||
|
||||
# No error - fields are unknown, so any keyword arguments are accepted
|
||||
config = GroundTruth(duration=0, video_fps=30, audio_sample_rate=44100)
|
||||
reveal_type(config) # revealed: GroundTruth
|
||||
reveal_type(config.duration) # revealed: Any
|
||||
|
||||
# Namedtuples with unknown fields inherit from tuple[Unknown, ...] to avoid false positives.
|
||||
# revealed: (<class 'GroundTruth'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
|
||||
reveal_mro(GroundTruth)
|
||||
|
||||
# No index-out-of-bounds error since the tuple length is unknown.
|
||||
reveal_type(config[0]) # revealed: Unknown
|
||||
reveal_type(config[100]) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Functional syntax signature validation
|
||||
|
||||
The `collections.namedtuple` function accepts `str | Iterable[str]` for `field_names`:
|
||||
|
||||
```py
|
||||
import collections
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
# String field names (space-separated)
|
||||
Point1 = collections.namedtuple("Point", "x y")
|
||||
reveal_type(Point1) # revealed: <class 'Point'>
|
||||
reveal_mro(Point1) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# String field names with multiple spaces
|
||||
Point1a = collections.namedtuple("Point", "x y")
|
||||
reveal_type(Point1a) # revealed: <class 'Point'>
|
||||
reveal_mro(Point1a) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# String field names (comma-separated also works at runtime)
|
||||
Point2 = collections.namedtuple("Point", "x, y")
|
||||
reveal_type(Point2) # revealed: <class 'Point'>
|
||||
reveal_mro(Point2) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# List of strings
|
||||
Point3 = collections.namedtuple("Point", ["x", "y"])
|
||||
reveal_type(Point3) # revealed: <class 'Point'>
|
||||
reveal_mro(Point3) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# Tuple of strings
|
||||
Point4 = collections.namedtuple("Point", ("x", "y"))
|
||||
reveal_type(Point4) # revealed: <class 'Point'>
|
||||
reveal_mro(Point4) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# Invalid: integer is not a valid typename
|
||||
# error: [invalid-argument-type]
|
||||
Invalid = collections.namedtuple(123, ["x", "y"])
|
||||
reveal_type(Invalid) # revealed: <class '<unknown>'>
|
||||
reveal_mro(Invalid) # revealed: (<class '<unknown>'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# Invalid: too many positional arguments
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `namedtuple`: expected 2, got 4"
|
||||
TooMany = collections.namedtuple("TooMany", "x", "y", "z")
|
||||
reveal_type(TooMany) # revealed: <class 'TooMany'>
|
||||
```
|
||||
|
||||
The `typing.NamedTuple` function accepts `Iterable[tuple[str, Any]]` for `fields`:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
# List of tuples
|
||||
Person1 = NamedTuple("Person", [("name", str), ("age", int)])
|
||||
reveal_type(Person1) # revealed: <class 'Person'>
|
||||
|
||||
# Tuple of tuples
|
||||
Person2 = NamedTuple("Person", (("name", str), ("age", int)))
|
||||
reveal_type(Person2) # revealed: <class 'Person'>
|
||||
|
||||
# Invalid: integer is not a valid typename
|
||||
# error: [invalid-argument-type]
|
||||
NamedTuple(123, [("name", str)])
|
||||
|
||||
# Invalid: too many positional arguments
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `NamedTuple`: expected 2, got 4"
|
||||
TooMany = NamedTuple("TooMany", [("x", int)], "extra", "args")
|
||||
reveal_type(TooMany) # revealed: <class 'TooMany'>
|
||||
```
|
||||
|
||||
### Keyword arguments for `collections.namedtuple`
|
||||
|
||||
The `collections.namedtuple` function accepts `rename`, `defaults`, and `module` keyword arguments:
|
||||
|
||||
```py
|
||||
import collections
|
||||
from ty_extensions import reveal_mro
|
||||
|
||||
# `rename=True` replaces invalid identifiers with positional names
|
||||
Point = collections.namedtuple("Point", ["x", "class", "_y", "z", "z"], rename=True)
|
||||
reveal_type(Point) # revealed: <class 'Point'>
|
||||
reveal_type(Point.__new__) # revealed: (cls: type, x: Any, _1: Any, _2: Any, z: Any, _4: Any) -> Point
|
||||
reveal_mro(Point) # revealed: (<class 'Point'>, <class 'tuple[Any, Any, Any, Any, Any]'>, <class 'object'>)
|
||||
p = Point(1, 2, 3, 4, 5)
|
||||
reveal_type(p.x) # revealed: Any
|
||||
reveal_type(p._1) # revealed: Any
|
||||
reveal_type(p._2) # revealed: Any
|
||||
reveal_type(p.z) # revealed: Any
|
||||
reveal_type(p._4) # revealed: Any
|
||||
|
||||
# Truthy non-bool values for `rename` are also handled, but emit a diagnostic
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter `rename` of `namedtuple()`"
|
||||
Point2 = collections.namedtuple("Point2", ["_x", "class"], rename=1)
|
||||
reveal_type(Point2) # revealed: <class 'Point2'>
|
||||
reveal_type(Point2.__new__) # revealed: (cls: type, _0: Any, _1: Any) -> Point2
|
||||
|
||||
# `defaults` provides default values for the rightmost fields
|
||||
Person = collections.namedtuple("Person", ["name", "age", "city"], defaults=["Unknown"])
|
||||
reveal_type(Person) # revealed: <class 'Person'>
|
||||
reveal_type(Person.__new__) # revealed: (cls: type, name: Any, age: Any, city: Any = "Unknown") -> Person
|
||||
|
||||
reveal_mro(Person) # revealed: (<class 'Person'>, <class 'tuple[Any, Any, Any]'>, <class 'object'>)
|
||||
# Can create with all fields
|
||||
person1 = Person("Alice", 30, "NYC")
|
||||
# Can omit the field with default
|
||||
person2 = Person("Bob", 25)
|
||||
reveal_type(person1.city) # revealed: Any
|
||||
reveal_type(person2.city) # revealed: Any
|
||||
|
||||
# `module` is valid but doesn't affect type checking
|
||||
Config = collections.namedtuple("Config", ["host", "port"], module="myapp")
|
||||
reveal_type(Config) # revealed: <class 'Config'>
|
||||
|
||||
# When more defaults are provided than fields, we treat all fields as having defaults.
|
||||
# TODO: This should emit a diagnostic since it would fail at runtime.
|
||||
TooManyDefaults = collections.namedtuple("TooManyDefaults", ["x", "y"], defaults=("a", "b", "c"))
|
||||
reveal_type(TooManyDefaults) # revealed: <class 'TooManyDefaults'>
|
||||
reveal_type(TooManyDefaults.__new__) # revealed: (cls: type, x: Any = "a", y: Any = "b") -> TooManyDefaults
|
||||
|
||||
# Unknown keyword arguments produce an error
|
||||
# error: [unknown-argument]
|
||||
Bad1 = collections.namedtuple("Bad1", ["x", "y"], foobarbaz=42)
|
||||
reveal_type(Bad1) # revealed: <class 'Bad1'>
|
||||
reveal_mro(Bad1) # revealed: (<class 'Bad1'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# Multiple unknown keyword arguments
|
||||
# error: [unknown-argument]
|
||||
# error: [unknown-argument]
|
||||
Bad2 = collections.namedtuple("Bad2", ["x"], invalid1=True, invalid2=False)
|
||||
reveal_type(Bad2) # revealed: <class 'Bad2'>
|
||||
reveal_mro(Bad2) # revealed: (<class 'Bad2'>, <class 'tuple[Any]'>, <class 'object'>)
|
||||
|
||||
# Invalid type for `defaults` (not Iterable[Any] | None)
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter `defaults` of `namedtuple()`"
|
||||
Bad3 = collections.namedtuple("Bad3", ["x"], defaults=123)
|
||||
reveal_type(Bad3) # revealed: <class 'Bad3'>
|
||||
|
||||
# Invalid type for `module` (not str | None)
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter `module` of `namedtuple()`"
|
||||
Bad4 = collections.namedtuple("Bad4", ["x"], module=456)
|
||||
reveal_type(Bad4) # revealed: <class 'Bad4'>
|
||||
|
||||
# Invalid type for `field_names` (not str | Iterable[str])
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter `field_names` of `namedtuple()`"
|
||||
Bad5 = collections.namedtuple("Bad5", 12345)
|
||||
reveal_type(Bad5) # revealed: <class 'Bad5'>
|
||||
```
|
||||
|
||||
### Keyword arguments for `typing.NamedTuple`
|
||||
|
||||
The `typing.NamedTuple` function does not accept any keyword arguments:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
# error: [unknown-argument]
|
||||
Bad3 = NamedTuple("Bad3", [("x", int)], rename=True)
|
||||
|
||||
# error: [unknown-argument]
|
||||
Bad4 = NamedTuple("Bad4", [("x", int)], defaults=[0])
|
||||
|
||||
# error: [unknown-argument]
|
||||
Bad5 = NamedTuple("Bad5", [("x", int)], foobarbaz=42)
|
||||
|
||||
# Invalid type for `fields` (not an iterable)
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter `fields` of `NamedTuple()`"
|
||||
Bad6 = NamedTuple("Bad6", 12345)
|
||||
reveal_type(Bad6) # revealed: <class 'Bad6'>
|
||||
```
|
||||
|
||||
### Starred and double-starred arguments
|
||||
|
||||
When starred (`*args`) or double-starred (`**kwargs`) arguments are used, we fall back to normal
|
||||
call binding since we can't statically determine the arguments. This results in `NamedTupleFallback`
|
||||
being returned:
|
||||
|
||||
```py
|
||||
import collections
|
||||
from typing import NamedTuple
|
||||
|
||||
args = ("Point", ["x", "y"])
|
||||
kwargs = {"rename": True}
|
||||
|
||||
# Starred positional arguments - falls back to NamedTupleFallback
|
||||
Point1 = collections.namedtuple(*args)
|
||||
reveal_type(Point1) # revealed: type[NamedTupleFallback]
|
||||
|
||||
# Ideally we'd catch this false negative,
|
||||
# but it's unclear if it's worth the added complexity
|
||||
Point2 = NamedTuple(*args)
|
||||
reveal_type(Point2) # revealed: type[NamedTupleFallback]
|
||||
|
||||
# Double-starred keyword arguments - falls back to NamedTupleFallback
|
||||
Point3 = collections.namedtuple("Point", ["x", "y"], **kwargs)
|
||||
reveal_type(Point3) # revealed: type[NamedTupleFallback]
|
||||
|
||||
Point4 = NamedTuple("Point", [("x", int), ("y", int)], **kwargs)
|
||||
reveal_type(Point4) # revealed: type[NamedTupleFallback]
|
||||
```
|
||||
|
||||
### Definition
|
||||
@@ -154,6 +627,84 @@ class D(
|
||||
class E(NamedTuple, Protocol): ...
|
||||
```
|
||||
|
||||
However, as explained above, for `class Foo(namedtuple("Foo", ...)): ...` the outer class is not
|
||||
itself a namedtuple—it just inherits from one. So it can use multiple inheritance freely:
|
||||
|
||||
```py
|
||||
from abc import ABC
|
||||
from collections import namedtuple
|
||||
from typing import NamedTuple
|
||||
|
||||
class Point(namedtuple("Point", ["x", "y"]), ABC):
|
||||
"""No error - functional namedtuple inheritance allows multiple inheritance."""
|
||||
|
||||
class Url(NamedTuple("Url", [("host", str), ("port", int)]), ABC):
|
||||
"""No error - typing.NamedTuple functional syntax also allows multiple inheritance."""
|
||||
|
||||
p = Point(1, 2)
|
||||
reveal_type(p.x) # revealed: Any
|
||||
reveal_type(p.y) # revealed: Any
|
||||
|
||||
u = Url("localhost", 8080)
|
||||
reveal_type(u.host) # revealed: str
|
||||
reveal_type(u.port) # revealed: int
|
||||
```
|
||||
|
||||
### Inherited tuple methods
|
||||
|
||||
Namedtuples inherit methods from their tuple base class, including `count`, `index`, and comparison
|
||||
methods (`__lt__`, `__le__`, `__gt__`, `__ge__`).
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
from typing import NamedTuple
|
||||
|
||||
# typing.NamedTuple inherits tuple methods
|
||||
class Point(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
p = Point(1, 2)
|
||||
reveal_type(p.count(1)) # revealed: int
|
||||
reveal_type(p.index(2)) # revealed: int
|
||||
|
||||
# collections.namedtuple also inherits tuple methods
|
||||
Person = namedtuple("Person", ["name", "age"])
|
||||
alice = Person("Alice", 30)
|
||||
reveal_type(alice.count("Alice")) # revealed: int
|
||||
reveal_type(alice.index(30)) # revealed: int
|
||||
```
|
||||
|
||||
The `@total_ordering` decorator should not emit a diagnostic, since the required `__lt__` method is
|
||||
already present:
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
from functools import total_ordering
|
||||
from typing import NamedTuple
|
||||
|
||||
# No error - __lt__ is inherited from the tuple base class
|
||||
@total_ordering
|
||||
class Point(namedtuple("Point", "x y")): ...
|
||||
|
||||
p1 = Point(1, 2)
|
||||
p2 = Point(3, 4)
|
||||
# TODO: should be `bool`, not `Any | Literal[False]`
|
||||
reveal_type(p1 < p2) # revealed: Any | Literal[False]
|
||||
reveal_type(p1 <= p2) # revealed: Any | Literal[True]
|
||||
|
||||
# Same for typing.NamedTuple - no error
|
||||
@total_ordering
|
||||
class Person(NamedTuple):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
alice = Person("Alice", 30)
|
||||
bob = Person("Bob", 25)
|
||||
reveal_type(alice < bob) # revealed: bool
|
||||
reveal_type(alice >= bob) # revealed: bool
|
||||
```
|
||||
|
||||
### Inheriting from a `NamedTuple`
|
||||
|
||||
Inheriting from a `NamedTuple` is supported, but new fields on the subclass will not be part of the
|
||||
@@ -254,6 +805,34 @@ reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
|
||||
reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float
|
||||
```
|
||||
|
||||
Generic namedtuples can also be defined using the functional syntax with type variables in the field
|
||||
types. We don't currently support this, but mypy does:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: ideally this would create a generic namedtuple class
|
||||
Pair = NamedTuple("Pair", [("first", T), ("second", T)])
|
||||
|
||||
# For now, the TypeVar is not specialized, so the field types remain as `T@Pair` and argument type
|
||||
# errors are emitted when calling the constructor.
|
||||
reveal_type(Pair) # revealed: <class 'Pair'>
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(Pair(1, 2)) # revealed: Pair
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(Pair(1, 2).first) # revealed: T@Pair
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(Pair(1, 2).second) # revealed: T@Pair
|
||||
```
|
||||
|
||||
## Attributes on `NamedTuple`
|
||||
|
||||
The following attributes are available on `NamedTuple` classes / instances:
|
||||
@@ -267,6 +846,7 @@ class Person(NamedTuple):
|
||||
|
||||
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
|
||||
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
|
||||
reveal_type(Person.__slots__) # revealed: tuple[()]
|
||||
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
|
||||
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
|
||||
reveal_type(Person._replace) # revealed: (self: Self, *, name: str = ..., age: int | None = ...) -> Self
|
||||
@@ -309,6 +889,75 @@ Person = namedtuple("Person", ["id", "name", "age"], defaults=[None])
|
||||
|
||||
alice = Person(1, "Alice", 42)
|
||||
bob = Person(2, "Bob")
|
||||
|
||||
reveal_type(Person.__slots__) # revealed: tuple[()]
|
||||
```
|
||||
|
||||
## `collections.namedtuple` with tuple variable field names
|
||||
|
||||
When field names are passed via a tuple variable, we can extract the literal field names from the
|
||||
inferred tuple type. The class is properly synthesized (not a fallback), but field types are `Any`
|
||||
since `collections.namedtuple` doesn't include type annotations:
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
|
||||
field_names = ("name", "age")
|
||||
Person = namedtuple("Person", field_names)
|
||||
|
||||
reveal_type(Person) # revealed: <class 'Person'>
|
||||
|
||||
alice = Person("Alice", 42)
|
||||
reveal_type(alice) # revealed: Person
|
||||
reveal_type(alice.name) # revealed: Any
|
||||
reveal_type(alice.age) # revealed: Any
|
||||
```
|
||||
|
||||
## `collections.namedtuple` with list variable field names
|
||||
|
||||
When field names are passed via a list variable (not a literal), we fall back to
|
||||
`NamedTupleFallback` which allows any attribute access. This is a regression test for accessing
|
||||
`Self` attributes in methods of classes that inherit from namedtuples with dynamic fields:
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
from typing_extensions import Self
|
||||
|
||||
field_names = ["host", "port"]
|
||||
|
||||
class Url(namedtuple("Url", field_names)):
|
||||
def with_port(self, port: int) -> Self:
|
||||
# Fields are unknown, so attribute access returns `Any`.
|
||||
reveal_type(self.host) # revealed: Any
|
||||
reveal_type(self.port) # revealed: Any
|
||||
reveal_type(self.unknown) # revealed: Any
|
||||
return self._replace(port=port)
|
||||
```
|
||||
|
||||
## `collections.namedtuple` attributes
|
||||
|
||||
Functional namedtuples have synthesized attributes similar to class-based namedtuples:
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
|
||||
Person = namedtuple("Person", ["name", "age"])
|
||||
|
||||
reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]]
|
||||
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
|
||||
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
|
||||
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
|
||||
reveal_type(Person._replace) # revealed: (self: Self, *, name: Any = ..., age: Any = ...) -> Self
|
||||
|
||||
# _make creates instances from an iterable.
|
||||
reveal_type(Person._make(["Alice", 30])) # revealed: Person
|
||||
|
||||
# _asdict converts to a dictionary.
|
||||
person = Person("Alice", 30)
|
||||
reveal_type(person._asdict()) # revealed: dict[str, Any]
|
||||
|
||||
# _replace creates a copy with replaced fields.
|
||||
reveal_type(person._replace(name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
## The symbol `NamedTuple` itself
|
||||
@@ -348,7 +997,7 @@ def expects_named_tuple(x: typing.NamedTuple):
|
||||
reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike
|
||||
reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike
|
||||
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(...) -> NamedTupleLike
|
||||
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]]
|
||||
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], [_T](value: tuple[_T, ...], /) -> tuple[object, ...]]
|
||||
reveal_type(x.__add__)
|
||||
reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object]
|
||||
|
||||
|
||||
@@ -69,6 +69,30 @@ def call_with_args(y: object, a: int, b: str) -> object:
|
||||
return None
|
||||
```
|
||||
|
||||
## Narrowing with named expressions (walrus operator)
|
||||
|
||||
When `callable()` is used with a named expression, the target of the named expression should be
|
||||
narrowed.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Foo:
|
||||
func: Any | None
|
||||
|
||||
def f(foo: Foo):
|
||||
first = getattr(foo, "func", None)
|
||||
if callable(first):
|
||||
reveal_type(first) # revealed: Any & Top[(...) -> object]
|
||||
else:
|
||||
reveal_type(first) # revealed: (Any & ~Top[(...) -> object]) | None
|
||||
|
||||
if callable(second := getattr(foo, "func", None)):
|
||||
reveal_type(second) # revealed: Any & Top[(...) -> object]
|
||||
else:
|
||||
reveal_type(second) # revealed: (Any & ~Top[(...) -> object]) | None
|
||||
```
|
||||
|
||||
## Assignability of narrowed callables
|
||||
|
||||
A narrowed callable `Top[Callable[..., object]]` should be assignable to `Callable[..., Any]`. This
|
||||
|
||||
@@ -12,6 +12,30 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
## `None != x` (reversed operands)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
if None != x:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
This also works for `==` with reversed operands:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
if None == x:
|
||||
reveal_type(x) # revealed: None
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## `!=` for other singleton types
|
||||
|
||||
### Bool
|
||||
|
||||
@@ -121,6 +121,31 @@ def test(x: Literal["a", "b", "c"] | None | int = None):
|
||||
reveal_type(x) # revealed: Literal["a", "c"] | int
|
||||
```
|
||||
|
||||
## No narrowing for the right-hand side (currently)
|
||||
|
||||
No narrowing is done for the right-hand side currently, even if the right-hand side is a valid
|
||||
"target" (name/attribute/subscript) that could potentially be narrowed. We may change this in the
|
||||
future:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(x: Literal["abc", "def"]):
|
||||
if "a" in x:
|
||||
# `x` could also be validly narrowed to `Literal["abc"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
else:
|
||||
# `x` could also be validly narrowed to `Literal["def"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
|
||||
if "a" not in x:
|
||||
# `x` could also be validly narrowed to `Literal["def"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
else:
|
||||
# `x` could also be validly narrowed to `Literal["abc"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
```
|
||||
|
||||
## bool
|
||||
|
||||
```py
|
||||
|
||||
@@ -16,6 +16,32 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `None is not x` (reversed operands)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
if None is not x:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
This also works for other singleton types with reversed operands:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = True if flag else False
|
||||
|
||||
if False is not x:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
Boolean literals:
|
||||
|
||||
@@ -556,3 +556,43 @@ def _(x: type[object], y: type[object], z: type[object]):
|
||||
if issubclass(z, Invariant):
|
||||
reveal_type(z) # revealed: type[Top[Invariant[Unknown]]]
|
||||
```
|
||||
|
||||
## Narrowing with TypedDict unions
|
||||
|
||||
Narrowing unions of `int` and multiple TypedDicts using `isinstance(x, dict)` should not panic
|
||||
during type ordering of normalized intersection types. Regression test for
|
||||
<https://github.com/astral-sh/ty/issues/2451>.
|
||||
|
||||
```py
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
class A(TypedDict):
|
||||
x: str
|
||||
|
||||
class B(TypedDict):
|
||||
y: str
|
||||
|
||||
T = int | A | B
|
||||
|
||||
def test(a: Any, items: list[T]) -> None:
|
||||
combined = a or items
|
||||
v = combined[0]
|
||||
if isinstance(v, dict):
|
||||
cast(T, v) # no panic
|
||||
```
|
||||
|
||||
## Narrowing with named expressions (walrus operator)
|
||||
|
||||
When `isinstance()` is used with a named expression, the target of the named expression should be
|
||||
narrowed.
|
||||
|
||||
```py
|
||||
def get_value() -> int | str:
|
||||
return 1
|
||||
|
||||
def f():
|
||||
if isinstance(x := get_value(), int):
|
||||
reveal_type(x) # revealed: int
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
@@ -347,3 +347,19 @@ def _(x: LiteralString):
|
||||
else:
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal[""]
|
||||
```
|
||||
|
||||
## Narrowing with named expressions (walrus operator)
|
||||
|
||||
When a truthiness check is used with a named expression, the target of the named expression should
|
||||
be narrowed.
|
||||
|
||||
```py
|
||||
def get_value() -> str | None:
|
||||
return "hello"
|
||||
|
||||
def f():
|
||||
if x := get_value():
|
||||
reveal_type(x) # revealed: str & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: (str & ~AlwaysTruthy) | None
|
||||
```
|
||||
|
||||
@@ -70,6 +70,47 @@ def _(x: A | B, y: A | C):
|
||||
reveal_type(y) # revealed: A
|
||||
```
|
||||
|
||||
## The top materialization is used for generic classes
|
||||
|
||||
```py
|
||||
# list is invariant
|
||||
def f(x: list[int] | None):
|
||||
if type(x) is list:
|
||||
reveal_type(x) # revealed: list[int]
|
||||
else:
|
||||
reveal_type(x) # revealed: list[int] | None
|
||||
|
||||
if type(x) is not list:
|
||||
reveal_type(x) # revealed: list[int] | None
|
||||
else:
|
||||
reveal_type(x) # revealed: list[int]
|
||||
|
||||
# frozenset is covariant
|
||||
def g(x: frozenset[bytes] | None):
|
||||
if type(x) is frozenset:
|
||||
reveal_type(x) # revealed: frozenset[bytes]
|
||||
else:
|
||||
reveal_type(x) # revealed: frozenset[bytes] | None
|
||||
|
||||
if type(x) is not frozenset:
|
||||
reveal_type(x) # revealed: frozenset[bytes] | None
|
||||
else:
|
||||
reveal_type(x) # revealed: frozenset[bytes]
|
||||
|
||||
def h(x: object):
|
||||
if type(x) is list:
|
||||
reveal_type(x) # revealed: Top[list[Unknown]]
|
||||
elif type(x) is frozenset:
|
||||
reveal_type(x) # revealed: frozenset[object]
|
||||
else:
|
||||
reveal_type(x) # revealed: object
|
||||
|
||||
if type(x) is not list and type(x) is not frozenset:
|
||||
reveal_type(x) # revealed: object
|
||||
else:
|
||||
reveal_type(x) # revealed: Top[list[Unknown]] | frozenset[object]
|
||||
```
|
||||
|
||||
## No narrowing for `type(x) is C[int]`
|
||||
|
||||
At runtime, `type(x)` will never return a generic alias object (only ever a class-literal object),
|
||||
@@ -134,12 +175,17 @@ class IsEqualToEverything(type):
|
||||
class A(metaclass=IsEqualToEverything): ...
|
||||
class B(metaclass=IsEqualToEverything): ...
|
||||
|
||||
def _(x: A | B):
|
||||
def _(x: A | B, y: object):
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(y) == bool:
|
||||
reveal_type(y) # revealed: object
|
||||
else:
|
||||
reveal_type(y) # revealed: object
|
||||
```
|
||||
|
||||
## No narrowing for custom `type` callable
|
||||
@@ -160,15 +206,19 @@ def _(x: A | B):
|
||||
|
||||
## No narrowing for multiple arguments
|
||||
|
||||
No narrowing should occur if `type` is used to dynamically create a class:
|
||||
Narrowing does not occur in the same way if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# The following diagnostic is valid, since the three-argument form of `type`
|
||||
# can only be called with `str` as the first argument.
|
||||
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
|
||||
#
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
# But we synthesize a new class object as the result of a three-argument call to `type`,
|
||||
# and we know that this synthesized class object is not the same object as the `str` class object,
|
||||
# so here the type is narrowed to `Never`!
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
@@ -228,8 +278,7 @@ An early version of <https://github.com/astral-sh/ruff/pull/19920> caused us to
|
||||
```py
|
||||
def _(val):
|
||||
if type(val) is tuple:
|
||||
# TODO: better would be `Unknown & tuple[object, ...]`
|
||||
reveal_type(val) # revealed: Unknown & tuple[Unknown, ...]
|
||||
reveal_type(val) # revealed: Unknown & tuple[object, ...]
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -14,8 +14,8 @@ def _(
|
||||
b: TypeIs[str | int],
|
||||
c: TypeGuard[bool],
|
||||
d: TypeIs[tuple[TypeOf[bytes]]],
|
||||
e: TypeGuard, # error: [invalid-type-form]
|
||||
f: TypeIs, # error: [invalid-type-form]
|
||||
e: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
|
||||
f: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
|
||||
):
|
||||
reveal_type(a) # revealed: TypeGuard[str]
|
||||
reveal_type(b) # revealed: TypeIs[str | int]
|
||||
@@ -46,12 +46,23 @@ A user-defined type guard must accept at least one positional argument (in addit
|
||||
for non-static methods).
|
||||
|
||||
```pyi
|
||||
from typing import Any, TypeVar
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
T = TypeVar("T")
|
||||
|
||||
# Multiple parameters are allowed
|
||||
def is_str_list(val: list[object], allow_empty: bool) -> TypeGuard[list[str]]: ...
|
||||
def is_set_of(val: set[Any], type: type[T]) -> TypeGuard[set[T]]: ...
|
||||
def is_two_element_tuple(val: tuple[object, ...], a: str, b: str) -> TypeIs[tuple[str, str]]: ...
|
||||
|
||||
# error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
def _() -> TypeGuard[str]: ...
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
# error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
def _(*args) -> TypeGuard[str]: ...
|
||||
|
||||
# error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
|
||||
def _(**kwargs) -> TypeIs[str]: ...
|
||||
|
||||
class _:
|
||||
@@ -63,14 +74,14 @@ class _:
|
||||
def _(a) -> TypeIs[str]: ...
|
||||
|
||||
# errors
|
||||
def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
def _(self) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
def _(self, /, *, a) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
@classmethod
|
||||
def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
def _(cls) -> TypeIs[str]: ... # error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
|
||||
@classmethod
|
||||
def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
def _() -> TypeIs[str]: ... # error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
|
||||
@staticmethod
|
||||
def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
|
||||
def _(*, a) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
```
|
||||
|
||||
For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter,
|
||||
@@ -86,10 +97,10 @@ def _(a: tuple[object]) -> TypeIs[tuple[str]]: ...
|
||||
def _(a: str | Any) -> TypeIs[str]: ...
|
||||
def _(a) -> TypeIs[str]: ...
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
# error: [invalid-type-guard-definition] "Narrowed type `str` is not assignable to the declared parameter type `int`"
|
||||
def _(a: int) -> TypeIs[str]: ...
|
||||
|
||||
# TODO: error: [invalid-type-guard-definition]
|
||||
# error: [invalid-type-guard-definition] "Narrowed type `int` is not assignable to the declared parameter type `bool | str`"
|
||||
def _(a: bool | str) -> TypeIs[int]: ...
|
||||
```
|
||||
|
||||
@@ -107,12 +118,14 @@ class C:
|
||||
@classmethod
|
||||
def g(cls, x: object) -> TypeGuard[int]:
|
||||
return True
|
||||
# TODO: this could error at definition time
|
||||
def h(self) -> TypeGuard[str]:
|
||||
|
||||
def h(
|
||||
self,
|
||||
) -> TypeGuard[str]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
return True
|
||||
# TODO: this could error at definition time
|
||||
|
||||
@classmethod
|
||||
def j(cls) -> TypeGuard[int]:
|
||||
def j(cls) -> TypeGuard[int]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
|
||||
return True
|
||||
|
||||
def _(x: object):
|
||||
@@ -221,7 +234,7 @@ def g(a: object) -> TypeIs[int]:
|
||||
return True
|
||||
|
||||
def _(d: Any):
|
||||
if f(): # error: [missing-argument]
|
||||
if f(): # error: [missing-argument] "No argument provided for required parameter `a` of function `f`"
|
||||
...
|
||||
|
||||
if g(*d):
|
||||
@@ -230,7 +243,7 @@ def _(d: Any):
|
||||
if f("foo"): # TODO: error: [invalid-type-guard-call]
|
||||
...
|
||||
|
||||
if g(a=d): # error: [invalid-type-guard-call]
|
||||
if g(a=d): # error: [invalid-type-guard-call] "Type guard call does not have a target"
|
||||
...
|
||||
```
|
||||
|
||||
@@ -499,3 +512,32 @@ def _(x: object):
|
||||
if f(x) and (g(x) or h(x)):
|
||||
reveal_type(x) # revealed: B | (A & C)
|
||||
```
|
||||
|
||||
## Narrowing with named expressions (walrus operator)
|
||||
|
||||
When a type guard is used with a named expression, the target of the named expression should be
|
||||
narrowed.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
def is_str(x: object) -> TypeIs[str]:
|
||||
return isinstance(x, str)
|
||||
|
||||
def guard_str(x: object) -> TypeGuard[str]:
|
||||
return isinstance(x, str)
|
||||
|
||||
def get_value() -> int | str:
|
||||
return 1
|
||||
|
||||
def f():
|
||||
if is_str(x := get_value()):
|
||||
reveal_type(x) # revealed: str
|
||||
else:
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
if guard_str(y := get_value()):
|
||||
reveal_type(y) # revealed: str
|
||||
else:
|
||||
reveal_type(y) # revealed: int | str
|
||||
```
|
||||
|
||||
@@ -311,7 +311,7 @@ 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@func) -> T@func]
|
||||
reveal_type(func) # revealed: Overload[() -> None, [T](x: T) -> T]
|
||||
reveal_type(func()) # revealed: None
|
||||
reveal_type(func(1)) # revealed: Literal[1]
|
||||
reveal_type(func("")) # revealed: Literal[""]
|
||||
|
||||
@@ -453,3 +453,17 @@ def _(y: Y):
|
||||
if isinstance(y, dict):
|
||||
reveal_type(y) # revealed: dict[str, X] | dict[str, Y]
|
||||
```
|
||||
|
||||
### Recursive alias with tuple - stack overflow test (issue 2470)
|
||||
|
||||
This test case used to cause a stack overflow. The returned type `list[int]` is not assignable to
|
||||
`RecursiveT = int | tuple[RecursiveT, ...]`, so we get an error.
|
||||
|
||||
```py
|
||||
type RecursiveT = int | tuple[RecursiveT, ...]
|
||||
|
||||
def foo(a: int, b: int) -> RecursiveT:
|
||||
some_intermediate_var = (a, b)
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `RecursiveT`, found `list[int]`"
|
||||
return list(some_intermediate_var)
|
||||
```
|
||||
|
||||
@@ -339,7 +339,7 @@ class A: ...
|
||||
|
||||
def f(x: A):
|
||||
# TODO: no error
|
||||
# error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12 | mdtest_snippet.A @ src/mdtest_snippet.py:13` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13`"
|
||||
# error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12:7 | mdtest_snippet.A @ src/mdtest_snippet.py:13:7` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13:7`"
|
||||
x = A()
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ def _(flag: bool) -> None:
|
||||
abs = 1
|
||||
chr: int = 1
|
||||
|
||||
reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T@abs], /) -> _T@abs)
|
||||
reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T], /) -> _T)
|
||||
reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str)
|
||||
```
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7:15'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user