Compare commits

..

3 Commits

Author SHA1 Message Date
David Peter
ea56e2415f [ty] Support field_specifiers for metaclass based transformers 2025-10-15 17:21:22 +02:00
David Peter
23543194fc Recognize custom field-specifier functions 2025-10-15 16:39:26 +02:00
David Peter
4dc88a0a5f [ty] Store field_specifiers 2025-10-15 15:07:19 +02:00
145 changed files with 2455 additions and 6410 deletions

View File

@@ -1,12 +1,3 @@
# Define serial test group for running tests sequentially.
[test-groups]
serial = { max-threads = 1 }
# Run ty file watching tests sequentially to avoid race conditions.
[[profile.default.overrides]]
filter = 'binary(file_watching)'
test-group = 'serial'
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).

View File

@@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true
indent_size = 2
[*.{rs,py,pyi,toml}]
[*.{rs,py,pyi}]
indent_size = 4
[*.snap]
@@ -18,3 +18,6 @@ trim_trailing_whitespace = false
[*.md]
max_line_length = 100
[*.toml]
indent_size = 4

View File

@@ -12,10 +12,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -23,7 +19,6 @@ env:
RUSTUP_MAX_RETRIES: 10
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.13"
NEXTEST_PROFILE: ci
jobs:
determine_changes:
@@ -242,7 +237,7 @@ jobs:
cargo-test-linux:
name: "cargo test (linux)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
@@ -276,6 +271,9 @@ jobs:
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
run: cargo test -p ty_python_semantic --test mdtest || true
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
# Check for broken links in the documentation.
@@ -301,13 +299,9 @@ jobs:
cargo-test-linux-release:
name: "cargo test (linux, release)"
# release builds timeout on GitHub runners, so this job is just skipped on forks in the `if` check
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
!contains(github.event.pull_request.labels.*.name, 'no-test') &&
(needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main')
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -331,11 +325,14 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
cargo-test-windows:
name: "cargo test (windows)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}
runs-on: depot-windows-2022-16
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
@@ -355,7 +352,9 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
@@ -386,6 +385,9 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
@@ -422,7 +424,7 @@ jobs:
cargo-build-msrv:
name: "cargo build (msrv)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-latest-8
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
@@ -443,6 +445,7 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build tests"
shell: bash
env:
MSRV: ${{ steps.msrv.outputs.value }}
run: cargo "+${MSRV}" test --no-run --all-features
@@ -535,7 +538,7 @@ jobs:
ecosystem:
name: "ecosystem"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-latest-8
needs:
- cargo-test-linux
- determine_changes
@@ -660,13 +663,13 @@ jobs:
fuzz-ty:
name: "Fuzz for new ty panics"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs:
- cargo-test-linux
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
timeout-minutes: 5
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -720,7 +723,7 @@ jobs:
ty-completion-evaluation:
name: "ty completion evaluation"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main' }}
steps:
@@ -766,7 +769,7 @@ jobs:
pre-commit:
name: "pre-commit"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -940,12 +943,8 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
(
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.formatter == 'true' ||
needs.determine_changes.outputs.linter == 'true'
)
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -979,11 +978,8 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
(
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
)
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:
- name: "Checkout Branch"

View File

@@ -19,10 +19,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -33,7 +29,7 @@ env:
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -53,6 +49,7 @@ jobs:
run: rustup show
- name: Run mypy_primer
shell: bash
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
DIFF_FILE: mypy_primer.diff
@@ -75,7 +72,7 @@ jobs:
memory_usage:
name: Run memory statistics
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -95,6 +92,7 @@ jobs:
run: rustup show
- name: Run mypy_primer
shell: bash
env:
TY_MAX_PARALLELISM: 1 # for deterministic memory numbers
TY_MEMORY_REPORT: mypy_primer

View File

@@ -16,10 +16,8 @@ name: Sync typeshed
# 3. Once the Windows worker is done, a MacOS worker:
# a. Checks out the branch created by the Linux worker
# b. Syncs all docstrings available on MacOS that are not available on Linux or Windows
# c. Attempts to update any snapshots that might have changed
# (this sub-step is allowed to fail)
# d. Commits the changes and pushes them to the same upstream branch
# e. Creates a PR against the `main` branch using the branch all three workers have pushed to
# c. Commits the changes and pushes them to the same upstream branch
# d. Creates a PR against the `main` branch using the branch all three workers have pushed to
# 4. If any of steps 1-3 failed, an issue is created in the `astral-sh/ruff` repository
on:
@@ -28,18 +26,8 @@ on:
# Run on the 1st and the 15th of every month:
- cron: "0 0 1,15 * *"
defaults:
run:
shell: bash
env:
# Don't set this flag globally for the workflow: it does strange things
# to the snapshots in the `cargo insta test --accept` step in the MacOS job.
#
# FORCE_COLOR: 1
CARGO_TERM_COLOR: always
NEXTEST_PROFILE: "ci"
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
# The name of the upstream branch that the first worker creates,
@@ -98,8 +86,6 @@ jobs:
git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
- name: Sync Linux docstrings
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
cd ruff
./scripts/codemod_docstrings.sh
@@ -138,8 +124,7 @@ jobs:
git config --global user.email '<>'
- name: Sync Windows docstrings
id: docstrings
env:
FORCE_COLOR: 1
shell: bash
run: ./scripts/codemod_docstrings.sh
- name: Commit the changes
if: ${{ steps.docstrings.outcome == 'success' }}
@@ -176,63 +161,26 @@ jobs:
git config --global user.name typeshedbot
git config --global user.email '<>'
- name: Sync macOS docstrings
run: ./scripts/codemod_docstrings.sh
- name: Commit and push the changes
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
./scripts/codemod_docstrings.sh
git commit -am "Sync macOS docstrings" --allow-empty
- name: Format the changes
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
# Here we just reformat the codemodded stubs so that they are
# consistent with the other typeshed stubs around them.
# Typeshed formats code using black in their CI, so we just invoke
# black on the stubs the same way that typeshed does.
uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
git commit -am "Format codemodded docstrings" --allow-empty
- name: Remove typeshed pyproject.toml file
if: ${{ success() }}
run: |
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
if: ${{ success() }}
run: rustup show
- name: "Install mold"
if: ${{ success() }}
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
- name: Update snapshots
if: ${{ success() }}
run: |
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
#
# If there were only snapshot-related failures, `cargo insta test --accept` will have exit code 0,
# but if there were also other mdtest failures (for example), it will return a nonzero exit code.
# We don't care about other tests failing here, we just want snapshots updated where possible,
# so we use `|| true` here to ignore the exit code.
cargo insta test --accept --color=always --all-features --test-runner=nextest || true
- name: Commit snapshot changes
if: ${{ success() }}
run: git commit -am "Update snapshots" || echo "No snapshot changes to commit"
- name: Push changes upstream and create a PR
if: ${{ success() }}
run: |
git push
- name: Create a PR
if: ${{ success() }}
run: |
gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"

View File

@@ -22,7 +22,7 @@ env:
jobs:
ty-ecosystem-analyzer:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
steps:

View File

@@ -19,7 +19,7 @@ env:
jobs:
ty-ecosystem-report:
name: Create ecosystem report
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@@ -29,7 +29,7 @@ env:
jobs:
typing_conformance:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@@ -1,63 +1,5 @@
# Changelog
## 0.14.1
Released on 2025-10-16.
### Preview features
- [formatter] Remove parentheses around multiple exception types on Python 3.14+ ([#20768](https://github.com/astral-sh/ruff/pull/20768))
- \[`flake8-bugbear`\] Omit annotation in preview fix for `B006` ([#20877](https://github.com/astral-sh/ruff/pull/20877))
- \[`flake8-logging-format`\] Avoid dropping implicitly concatenated pieces in the `G004` fix ([#20793](https://github.com/astral-sh/ruff/pull/20793))
- \[`pydoclint`\] Implement `docstring-extraneous-parameter` (`DOC102`) ([#20376](https://github.com/astral-sh/ruff/pull/20376))
- \[`pyupgrade`\] Extend `UP019` to detect `typing_extensions.Text` (`UP019`) ([#20825](https://github.com/astral-sh/ruff/pull/20825))
- \[`pyupgrade`\] Fix false negative for `TypeVar` with default argument in `non-pep695-generic-class` (`UP046`) ([#20660](https://github.com/astral-sh/ruff/pull/20660))
### Bug fixes
- Fix false negatives in `Truthiness::from_expr` for lambdas, generators, and f-strings ([#20704](https://github.com/astral-sh/ruff/pull/20704))
- Fix syntax error false positives for escapes and quotes in f-strings ([#20867](https://github.com/astral-sh/ruff/pull/20867))
- Fix syntax error false positives on parenthesized context managers ([#20846](https://github.com/astral-sh/ruff/pull/20846))
- \[`fastapi`\] Fix false positives for path parameters that FastAPI doesn't recognize (`FAST003`) ([#20687](https://github.com/astral-sh/ruff/pull/20687))
- \[`flake8-pyi`\] Fix operator precedence by adding parentheses when needed (`PYI061`) ([#20508](https://github.com/astral-sh/ruff/pull/20508))
- \[`ruff`\] Suppress diagnostic for f-string interpolations with debug text (`RUF010`) ([#20525](https://github.com/astral-sh/ruff/pull/20525))
### Rule changes
- \[`airflow`\] Add warning to `airflow.datasets.DatasetEvent` usage (`AIR301`) ([#20551](https://github.com/astral-sh/ruff/pull/20551))
- \[`flake8-bugbear`\] Mark `B905` and `B912` fixes as unsafe ([#20695](https://github.com/astral-sh/ruff/pull/20695))
- Use `DiagnosticTag` for more rules - changes display in editors ([#20758](https://github.com/astral-sh/ruff/pull/20758),[#20734](https://github.com/astral-sh/ruff/pull/20734))
### Documentation
- Update Python compatibility from 3.13 to 3.14 in README.md ([#20852](https://github.com/astral-sh/ruff/pull/20852))
- Update `lint.flake8-type-checking.quoted-annotations` docs ([#20765](https://github.com/astral-sh/ruff/pull/20765))
- Update setup instructions for Zed 0.208.0+ ([#20902](https://github.com/astral-sh/ruff/pull/20902))
- \[`flake8-datetimez`\] Clarify docs for several rules ([#20778](https://github.com/astral-sh/ruff/pull/20778))
- Fix typo in `RUF015` description ([#20873](https://github.com/astral-sh/ruff/pull/20873))
### Other changes
- Reduce binary size ([#20863](https://github.com/astral-sh/ruff/pull/20863))
- Improved error recovery for unclosed strings (including f- and t-strings) ([#20848](https://github.com/astral-sh/ruff/pull/20848))
### Contributors
- [@ntBre](https://github.com/ntBre)
- [@Paillat-dev](https://github.com/Paillat-dev)
- [@terror](https://github.com/terror)
- [@pieterh-oai](https://github.com/pieterh-oai)
- [@MichaReiser](https://github.com/MichaReiser)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@ageorgou](https://github.com/ageorgou)
- [@danparizher](https://github.com/danparizher)
- [@mgaitan](https://github.com/mgaitan)
- [@augustelalande](https://github.com/augustelalande)
- [@dylwil3](https://github.com/dylwil3)
- [@Lee-W](https://github.com/Lee-W)
- [@injust](https://github.com/injust)
- [@CarrotManMatt](https://github.com/CarrotManMatt)
## 0.14.0
Released on 2025-10-07.

18
Cargo.lock generated
View File

@@ -2815,7 +2815,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"anyhow",
"argfile",
@@ -3072,7 +3072,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3426,7 +3426,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3540,8 +3540,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"boxcar",
"compact_str",
@@ -3564,13 +3564,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef9f9329be6923acd050c8dddd172e3bc93e8051", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "29ab321b45d00daa4315fa2a06f7207759a8c87e", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.1
rev: v0.14.0
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.1"
version = "0.14.0"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -13,6 +13,7 @@ use log::{debug, warn};
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
use ruff_linter::message::create_syntax_error_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
@@ -102,7 +103,11 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Diagnostic::invalid_syntax(dummy, err, TextRange::default())],
vec![create_syntax_error_diagnostic(
dummy,
err,
TextRange::default(),
)],
FxHashMap::default(),
)
}

View File

@@ -84,14 +84,17 @@ impl Diagnostic {
/// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of
/// the other way around. And since we want to do this conversion in a couple
/// places, it makes sense to centralize it _somewhere_. So it's here for now.
///
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
/// message.
pub fn invalid_syntax(
span: impl Into<Span>,
message: impl IntoDiagnosticMessage,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag.annotate(Annotation::primary(span).message(message));
diag
}

View File

@@ -193,17 +193,6 @@ impl Files {
roots.at(&absolute)
}
/// The same as [`Self::root`] but panics if no root is found.
#[track_caller]
pub fn expect_root(&self, db: &dyn Db, path: &SystemPath) -> FileRoot {
if let Some(root) = self.root(db, path) {
return root;
}
let roots = self.inner.roots.read().unwrap();
panic!("No root found for path '{path}'. Known roots: {roots:#?}");
}
/// Adds a new root for `path` and returns the root.
///
/// The root isn't added nor is the file root's kind updated if a root for `path` already exists.

View File

@@ -81,8 +81,6 @@ impl FileRoots {
}
}
tracing::debug!("Adding new file root '{path}' of kind {kind:?}");
// normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters
let mut route = normalized_path.replace('{', "{{").replace('}', "}}");

View File

@@ -98,10 +98,6 @@ impl Db for ModuleDb {
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
fn verbose(&self) -> bool {
false
}
}
#[salsa::db]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.1"
version = "0.14.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -11,7 +11,7 @@ from airflow import (
)
from airflow.api_connexion.security import requires_access
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent, DatasetEvent
from airflow.datasets import DatasetAliasEvent
from airflow.operators.postgres_operator import Mapping
from airflow.operators.subdag import SubDagOperator
from airflow.secrets.cache import SecretCache
@@ -48,7 +48,6 @@ AWSAthenaHook()
# airflow.datasets
DatasetAliasEvent()
DatasetEvent()
# airflow.operators.subdag.*

View File

@@ -78,11 +78,3 @@ b: None | Literal[None] | None
c: (None | Literal[None]) | None
d: None | (Literal[None] | None)
e: None | ((None | Literal[None]) | None) | None
# Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -1,264 +0,0 @@
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
a (int): The first number to add.
b (int): The second number to add.
Returns:
int: The sum of the two numbers.
"""
return a + b
# DOC102
def multiply_list_elements(lst):
"""
Multiplies each element in a list by a given multiplier.
Args:
lst (list of int): A list of integers.
multiplier (int): The multiplier for each element in the list.
Returns:
list of int: A new list with each element multiplied.
"""
return [x * multiplier for x in lst]
# DOC102
def find_max_value():
"""
Finds the maximum value in a list of numbers.
Args:
numbers (list of int): A list of integers to search through.
Returns:
int: The maximum value found in the list.
"""
return max(numbers)
# DOC102
def create_user_profile(location="here"):
"""
Creates a user profile with basic information.
Args:
name (str): The name of the user.
age (int): The age of the user.
email (str): The user's email address.
location (str): The location of the user.
Returns:
dict: A dictionary containing the user's profile.
"""
return {
'name': name,
'age': age,
'email': email,
'location': location
}
# DOC102
def calculate_total_price(item_prices, discount):
"""
Calculates the total price after applying tax and a discount.
Args:
item_prices (list of float): A list of prices for each item.
tax_rate (float): The tax rate to apply.
discount (float): The discount to subtract from the total.
Returns:
float: The final total price after tax and discount.
"""
total = sum(item_prices)
total_with_tax = total + (total * tax_rate)
final_total = total_with_tax - discount
return final_total
# DOC102
def send_email(subject, body, bcc_address=None):
"""
Sends an email to the specified recipients.
Args:
subject (str): The subject of the email.
body (str): The content of the email.
to_address (str): The recipient's email address.
cc_address (str, optional): The email address for CC. Defaults to None.
bcc_address (str, optional): The email address for BCC. Defaults to None.
Returns:
bool: True if the email was sent successfully, False otherwise.
"""
return True
# DOC102
def concatenate_strings(*args):
"""
Concatenates multiple strings with a specified separator.
Args:
separator (str): The separator to use between strings.
*args (str): Variable length argument list of strings to concatenate.
Returns:
str: A single concatenated string.
"""
return separator.join(args)
# DOC102
def process_order(order_id):
"""
Processes an order with a list of items and optional order details.
Args:
order_id (int): The unique identifier for the order.
*items (str): Variable length argument list of items in the order.
**details (dict): Additional details such as shipping method and address.
Returns:
dict: A dictionary containing the order summary.
"""
return {
'order_id': order_id,
'items': items,
'details': details
}
class Calculator:
"""
A simple calculator class that can perform basic arithmetic operations.
"""
# DOC102
def __init__(self):
"""
Initializes the calculator with an initial value.
Args:
value (int, optional): The initial value of the calculator. Defaults to 0.
"""
self.value = value
# DOC102
def add(self, number2):
"""
Adds a number to the current value.
Args:
number (int or float): The number to add to the current value.
Returns:
int or float: The updated value after addition.
"""
self.value += number + number2
return self.value
# DOC102
@classmethod
def from_string(cls):
"""
Creates a Calculator instance from a string representation of a number.
Args:
value_str (str): The string representing the initial value.
Returns:
Calculator: A new instance of Calculator initialized with the value from the string.
"""
value = float(value_str)
return cls(value)
# DOC102
@staticmethod
def is_valid_number():
"""
Checks if a given number is valid (int or float).
Args:
number (any): The value to check.
Returns:
bool: True if the number is valid, False otherwise.
"""
return isinstance(number, (int, float))
# OK
def foo(param1, param2, *args, **kwargs):
"""Foo.
Args:
param1 (int): The first parameter.
param2 (:obj:`str`, optional): The second parameter. Defaults to None.
Second line of description: should be indented.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
return
# OK
def on_server_unloaded(self, server_context: ServerContext) -> None:
''' Execute ``on_server_unloaded`` from ``server_lifecycle.py`` (if
it is defined) when the server cleanly exits. (Before stopping the
server's ``IOLoop``.)
Args:
server_context (ServerContext) :
.. warning::
In practice this code may not run, since servers are often killed
by a signal.
'''
return self._lifecycle_handler.on_server_unloaded(server_context)
# OK
def function_with_kwargs(param1, param2, **kwargs):
"""Function with **kwargs parameter.
Args:
param1 (int): The first parameter.
param2 (str): The second parameter.
extra_param (str): An extra parameter that may be passed via **kwargs.
another_extra (int): Another extra parameter.
"""
return
# OK
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
b: The second number to add.
Returns:
int: The sum of the two numbers.
"""
return
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
a: The first number to add.
b: The second number to add.
Returns:
int: The sum of the two numbers.
"""
return a + b

View File

@@ -1,372 +0,0 @@
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
a : int
The first number to add.
b : int
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
# DOC102
def multiply_list_elements(lst):
"""
Multiplies each element in a list by a given multiplier.
Parameters
----------
lst : list of int
A list of integers.
multiplier : int
The multiplier for each element in the list.
Returns
-------
list of int
A new list with each element multiplied.
"""
return [x * multiplier for x in lst]
# DOC102
def find_max_value():
"""
Finds the maximum value in a list of numbers.
Parameters
----------
numbers : list of int
A list of integers to search through.
Returns
-------
int
The maximum value found in the list.
"""
return max(numbers)
# DOC102
def create_user_profile(location="here"):
"""
Creates a user profile with basic information.
Parameters
----------
name : str
The name of the user.
age : int
The age of the user.
email : str
The user's email address.
location : str, optional
The location of the user, by default "here".
Returns
-------
dict
A dictionary containing the user's profile.
"""
return {
'name': name,
'age': age,
'email': email,
'location': location
}
# DOC102
def calculate_total_price(item_prices, discount):
"""
Calculates the total price after applying tax and a discount.
Parameters
----------
item_prices : list of float
A list of prices for each item.
tax_rate : float
The tax rate to apply.
discount : float
The discount to subtract from the total.
Returns
-------
float
The final total price after tax and discount.
"""
total = sum(item_prices)
total_with_tax = total + (total * tax_rate)
final_total = total_with_tax - discount
return final_total
# DOC102
def send_email(subject, body, bcc_address=None):
"""
Sends an email to the specified recipients.
Parameters
----------
subject : str
The subject of the email.
body : str
The content of the email.
to_address : str
The recipient's email address.
cc_address : str, optional
The email address for CC, by default None.
bcc_address : str, optional
The email address for BCC, by default None.
Returns
-------
bool
True if the email was sent successfully, False otherwise.
"""
return True
# DOC102
def concatenate_strings(*args):
"""
Concatenates multiple strings with a specified separator.
Parameters
----------
separator : str
The separator to use between strings.
*args : str
Variable length argument list of strings to concatenate.
Returns
-------
str
A single concatenated string.
"""
return True
# DOC102
def process_order(order_id):
"""
Processes an order with a list of items and optional order details.
Parameters
----------
order_id : int
The unique identifier for the order.
*items : str
Variable length argument list of items in the order.
**details : dict
Additional details such as shipping method and address.
Returns
-------
dict
A dictionary containing the order summary.
"""
return {
'order_id': order_id,
'items': items,
'details': details
}
class Calculator:
"""
A simple calculator class that can perform basic arithmetic operations.
"""
# DOC102
def __init__(self):
"""
Initializes the calculator with an initial value.
Parameters
----------
value : int, optional
The initial value of the calculator, by default 0.
"""
self.value = value
# DOC102
def add(self, number2):
"""
Adds two numbers to the current value.
Parameters
----------
number : int or float
The first number to add.
number2 : int or float
The second number to add.
Returns
-------
int or float
The updated value after addition.
"""
self.value += number + number2
return self.value
# DOC102
@classmethod
def from_string(cls):
"""
Creates a Calculator instance from a string representation of a number.
Parameters
----------
value_str : str
The string representing the initial value.
Returns
-------
Calculator
A new instance of Calculator initialized with the value from the string.
"""
value = float(value_str)
return cls(value)
# DOC102
@staticmethod
def is_valid_number():
"""
Checks if a given number is valid (int or float).
Parameters
----------
number : any
The value to check.
Returns
-------
bool
True if the number is valid, False otherwise.
"""
return isinstance(number, (int, float))
# OK
def function_with_kwargs(param1, param2, **kwargs):
"""Function with **kwargs parameter.
Parameters
----------
param1 : int
The first parameter.
param2 : str
The second parameter.
extra_param : str
An extra parameter that may be passed via **kwargs.
another_extra : int
Another extra parameter.
"""
return True
# OK
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
b
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
a
The first number to add.
b
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
class Foo:
# OK
def send_help(self, *args: Any) -> Any:
"""|coro|
Shows the help command for the specified entity if given.
The entity can be a command or a cog.
If no entity is given, then it'll show help for the
entire bot.
If the entity is a string, then it looks up whether it's a
:class:`Cog` or a :class:`Command`.
.. note::
Due to the way this function works, instead of returning
something similar to :meth:`~.commands.HelpCommand.command_not_found`
this returns :class:`None` on bad input or no help command.
Parameters
----------
entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]]
The entity to show help for.
Returns
-------
Any
The result of the help command, if any.
"""
return
# OK
@classmethod
async def convert(cls, ctx: Context, argument: str) -> Self:
"""|coro|
The method that actually converters an argument to the flag mapping.
Parameters
----------
cls: Type[:class:`FlagConverter`]
The flag converter class.
ctx: :class:`Context`
The invocation context.
argument: :class:`str`
The argument to convert from.
Raises
------
FlagError
A flag related parsing error.
CommandError
A command related error.
Returns
-------
:class:`FlagConverter`
The flag converter instance with all flags parsed.
"""
return

View File

@@ -43,7 +43,7 @@ class Foo:
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# `default` was added in Python 3.13
# `default` should be skipped for now, added in Python 3.13
T = typing.TypeVar("T", default=Any)
x: typing.TypeAlias = list[T]
@@ -90,9 +90,9 @@ PositiveList = TypeAliasType(
"PositiveList2", list[Annotated[T, Gt(0)]], type_params=(T,)
)
# `default` was added in Python 3.13
# `default` should be skipped for now, added in Python 3.13
T = typing.TypeVar("T", default=Any)
AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
AnyList = TypeAliasType("AnyList", list[T], typep_params=(T,))
# unsafe fix if comments within the fix
T = TypeVar("T")
@@ -128,7 +128,3 @@ T: TypeAlias = ( # comment0
str # comment6
# comment7
) # comment8
# Test case for TypeVar with default - should be converted when preview mode is enabled
T_default = TypeVar("T_default", default=int)
DefaultList: TypeAlias = list[T_default]

View File

@@ -122,7 +122,7 @@ class MixedGenerics[U]:
return (u, t)
# default requires 3.13
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
@@ -130,14 +130,6 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
var: V
# Test case for TypeVar with default but no bound
W = TypeVar("W", default=int)
class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
var: W
# nested classes and functions are skipped
class Outer:
class Inner(Generic[T]):

View File

@@ -44,7 +44,9 @@ def any_str_param(s: AnyStr) -> AnyStr:
return s
# default requires 3.13
# these cases are not handled
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
@@ -52,8 +54,6 @@ def default_var(v: V) -> V:
return v
# these cases are not handled
def outer():
def inner(t: T) -> T:
return t

View File

@@ -81,7 +81,6 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::UndocumentedPublicPackage,
]);
let enforce_pydoclint = checker.any_rule_enabled(&[
Rule::DocstringExtraneousParameter,
Rule::DocstringMissingReturns,
Rule::DocstringExtraneousReturns,
Rule::DocstringMissingYields,

View File

@@ -50,6 +50,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
}
}
Stmt::Continue(_) => {
if checker.is_rule_enabled(Rule::ContinueOutsideLoop) {
pyflakes::rules::continue_outside_loop(
checker,
stmt,
&mut checker.semantic.current_statements().skip(1),
);
}
}
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
is_async,

View File

@@ -711,12 +711,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(pyflakes::rules::BreakOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ContinueOutsideLoop => {
// F702
if self.is_rule_enabled(Rule::ContinueOutsideLoop) {
self.report_diagnostic(pyflakes::rules::ContinueOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)

View File

@@ -988,7 +988,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(FastApi, "003") => (RuleGroup::Stable, rules::fastapi::rules::FastApiUnusedPathParameter),
// pydoclint
(Pydoclint, "102") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousParameter),
(Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns),
(Pydoclint, "202") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousReturns),
(Pydoclint, "402") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingYields),

View File

@@ -24,6 +24,7 @@ use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{FixResult, fix_file};
use crate::message::create_syntax_error_diagnostic;
use crate::noqa::add_noqa;
use crate::package::PackageRoot;
use crate::registry::Rule;
@@ -495,15 +496,15 @@ fn diagnostics_to_messages(
parse_errors
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
Diagnostic::invalid_syntax(source_file.clone(), syntax_error, syntax_error)
create_syntax_error_diagnostic(source_file.clone(), syntax_error, syntax_error)
}))
.chain(
semantic_syntax_errors
.iter()
.map(|error| Diagnostic::invalid_syntax(source_file.clone(), error, error)),
.map(|error| create_syntax_error_diagnostic(source_file.clone(), error, error)),
)
.chain(diagnostics.into_iter().map(|mut diagnostic| {
if let Some(range) = diagnostic.range() {

View File

@@ -16,7 +16,7 @@ use ruff_db::files::File;
pub use grouped::GroupedEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter;
use crate::Fix;
@@ -26,6 +26,24 @@ use crate::settings::types::{OutputFormat, RuffOutputFormat};
mod grouped;
mod sarif;
/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff.
///
/// This is almost identical to `ruff_db::diagnostic::create_syntax_error_diagnostic`, except the
/// `message` is stored as the primary diagnostic message instead of on the primary annotation.
///
/// TODO(brent) These should be unified at some point, but we keep them separate for now to avoid a
/// ton of snapshot changes while combining ruff's diagnostic type with `Diagnostic`.
pub fn create_syntax_error_diagnostic(
span: impl Into<Span>,
message: impl std::fmt::Display,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag
}
/// Create a `Diagnostic` from a panic.
pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagnostic {
let mut diagnostic = Diagnostic::new(
@@ -242,6 +260,8 @@ mod tests {
use crate::message::{Emitter, EmitterContext, create_lint_diagnostic};
use crate::{Edit, Fix};
use super::create_syntax_error_diagnostic;
pub(super) fn create_syntax_error_diagnostics() -> Vec<Diagnostic> {
let source = r"from os import
@@ -254,7 +274,7 @@ if call(foo
.errors()
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.collect()
}

View File

@@ -242,11 +242,6 @@ pub(crate) const fn is_refined_submodule_import_match_enabled(settings: &LinterS
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20660
pub(crate) const fn is_type_var_default_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// github.com/astral-sh/ruff/issues/20004
pub(crate) const fn is_b006_check_guaranteed_mutable_expr_enabled(
settings: &LinterSettings,

View File

@@ -655,11 +655,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
// airflow.datasets
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
["airflow", "datasets", "DatasetEvent"] => Replacement::Message(
"`DatasetEvent` has been made private in Airflow 3. \
Use `dict[str, Any]` for the time being. \
An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version.",
),
// airflow.hooks
["airflow", "hooks", "base_hook", "BaseHook"] => Replacement::Rename {

View File

@@ -104,49 +104,38 @@ AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
49 | # airflow.datasets
50 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^
51 | DatasetEvent()
|
AIR301 `airflow.datasets.DatasetEvent` is removed in Airflow 3.0
--> AIR301_names.py:51:1
|
49 | # airflow.datasets
50 | DatasetAliasEvent()
51 | DatasetEvent()
| ^^^^^^^^^^^^
|
help: `DatasetEvent` has been made private in Airflow 3. Use `dict[str, Any]` for the time being. An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version.
AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
--> AIR301_names.py:55:1
--> AIR301_names.py:54:1
|
54 | # airflow.operators.subdag.*
55 | SubDagOperator()
53 | # airflow.operators.subdag.*
54 | SubDagOperator()
| ^^^^^^^^^^^^^^
56 |
57 | # airflow.operators.postgres_operator
55 |
56 | # airflow.operators.postgres_operator
|
help: The whole `airflow.subdag` module has been removed.
AIR301 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR301_names.py:58:1
--> AIR301_names.py:57:1
|
57 | # airflow.operators.postgres_operator
58 | Mapping()
56 | # airflow.operators.postgres_operator
57 | Mapping()
| ^^^^^^^
59 |
60 | # airflow.secrets
58 |
59 | # airflow.secrets
|
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
--> AIR301_names.py:65:1
--> AIR301_names.py:64:1
|
64 | # airflow.secrets.cache
65 | SecretCache()
63 | # airflow.secrets.cache
64 | SecretCache()
| ^^^^^^^^^^^
|
help: Use `SecretCache` from `airflow.sdk` instead.
14 | from airflow.datasets import DatasetAliasEvent, DatasetEvent
14 | from airflow.datasets import DatasetAliasEvent
15 | from airflow.operators.postgres_operator import Mapping
16 | from airflow.operators.subdag import SubDagOperator
- from airflow.secrets.cache import SecretCache
@@ -164,211 +153,211 @@ help: Use `SecretCache` from `airflow.sdk` instead.
note: This is an unsafe fix and may change runtime behavior
AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
--> AIR301_names.py:69:1
--> AIR301_names.py:68:1
|
68 | # airflow.triggers.external_task
69 | TaskStateTrigger()
67 | # airflow.triggers.external_task
68 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^
70 |
71 | # airflow.utils.date
69 |
70 | # airflow.utils.date
|
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:72:1
--> AIR301_names.py:71:1
|
71 | # airflow.utils.date
72 | dates.date_range
70 | # airflow.utils.date
71 | dates.date_range
| ^^^^^^^^^^^^^^^^
73 | dates.days_ago
72 | dates.days_ago
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:73:1
--> AIR301_names.py:72:1
|
71 | # airflow.utils.date
72 | dates.date_range
73 | dates.days_ago
70 | # airflow.utils.date
71 | dates.date_range
72 | dates.days_ago
| ^^^^^^^^^^^^^^
74 |
75 | date_range
73 |
74 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:75:1
--> AIR301_names.py:74:1
|
73 | dates.days_ago
74 |
75 | date_range
72 | dates.days_ago
73 |
74 | date_range
| ^^^^^^^^^^
76 | days_ago
77 | infer_time_unit
75 | days_ago
76 | infer_time_unit
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:76:1
--> AIR301_names.py:75:1
|
75 | date_range
76 | days_ago
74 | date_range
75 | days_ago
| ^^^^^^^^
77 | infer_time_unit
78 | parse_execution_date
76 | infer_time_unit
77 | parse_execution_date
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
--> AIR301_names.py:77:1
--> AIR301_names.py:76:1
|
75 | date_range
76 | days_ago
77 | infer_time_unit
74 | date_range
75 | days_ago
76 | infer_time_unit
| ^^^^^^^^^^^^^^^
78 | parse_execution_date
79 | round_time
77 | parse_execution_date
78 | round_time
|
AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
--> AIR301_names.py:78:1
--> AIR301_names.py:77:1
|
76 | days_ago
77 | infer_time_unit
78 | parse_execution_date
75 | days_ago
76 | infer_time_unit
77 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^
79 | round_time
80 | scale_time_units
78 | round_time
79 | scale_time_units
|
AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
--> AIR301_names.py:79:1
--> AIR301_names.py:78:1
|
77 | infer_time_unit
78 | parse_execution_date
79 | round_time
76 | infer_time_unit
77 | parse_execution_date
78 | round_time
| ^^^^^^^^^^
80 | scale_time_units
79 | scale_time_units
|
AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
--> AIR301_names.py:80:1
--> AIR301_names.py:79:1
|
78 | parse_execution_date
79 | round_time
80 | scale_time_units
77 | parse_execution_date
78 | round_time
79 | scale_time_units
| ^^^^^^^^^^^^^^^^
81 |
82 | # This one was not deprecated.
80 |
81 | # This one was not deprecated.
|
AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
--> AIR301_names.py:87:1
--> AIR301_names.py:86:1
|
86 | # airflow.utils.dag_cycle_tester
87 | test_cycle
85 | # airflow.utils.dag_cycle_tester
86 | test_cycle
| ^^^^^^^^^^
|
AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
--> AIR301_names.py:91:1
--> AIR301_names.py:90:1
|
90 | # airflow.utils.db
91 | create_session
89 | # airflow.utils.db
90 | create_session
| ^^^^^^^^^^^^^^
92 |
93 | # airflow.utils.decorators
91 |
92 | # airflow.utils.decorators
|
AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
--> AIR301_names.py:94:1
--> AIR301_names.py:93:1
|
93 | # airflow.utils.decorators
94 | apply_defaults
92 | # airflow.utils.decorators
93 | apply_defaults
| ^^^^^^^^^^^^^^
95 |
96 | # airflow.utils.file
94 |
95 | # airflow.utils.file
|
help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
--> AIR301_names.py:97:1
--> AIR301_names.py:96:1
|
96 | # airflow.utils.file
97 | mkdirs
95 | # airflow.utils.file
96 | mkdirs
| ^^^^^^
|
help: Use `pathlib.Path({path}).mkdir` instead
AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
--> AIR301_names.py:101:1
--> AIR301_names.py:100:1
|
100 | # airflow.utils.state
101 | SHUTDOWN
99 | # airflow.utils.state
100 | SHUTDOWN
| ^^^^^^^^
102 | terminating_states
101 | terminating_states
|
AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
--> AIR301_names.py:102:1
--> AIR301_names.py:101:1
|
100 | # airflow.utils.state
101 | SHUTDOWN
102 | terminating_states
99 | # airflow.utils.state
100 | SHUTDOWN
101 | terminating_states
| ^^^^^^^^^^^^^^^^^^
103 |
104 | # airflow.utils.trigger_rule
102 |
103 | # airflow.utils.trigger_rule
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
--> AIR301_names.py:105:1
--> AIR301_names.py:104:1
|
104 | # airflow.utils.trigger_rule
105 | TriggerRule.DUMMY
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
| ^^^^^^^^^^^^^^^^^
106 | TriggerRule.NONE_FAILED_OR_SKIPPED
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
--> AIR301_names.py:106:1
--> AIR301_names.py:105:1
|
104 | # airflow.utils.trigger_rule
105 | TriggerRule.DUMMY
106 | TriggerRule.NONE_FAILED_OR_SKIPPED
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
--> AIR301_names.py:110:1
--> AIR301_names.py:109:1
|
109 | # airflow.www.auth
110 | has_access
108 | # airflow.www.auth
109 | has_access
| ^^^^^^^^^^
111 | has_access_dataset
110 | has_access_dataset
|
AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0
--> AIR301_names.py:111:1
--> AIR301_names.py:110:1
|
109 | # airflow.www.auth
110 | has_access
111 | has_access_dataset
108 | # airflow.www.auth
109 | has_access
110 | has_access_dataset
| ^^^^^^^^^^^^^^^^^^
112 |
113 | # airflow.www.utils
111 |
112 | # airflow.www.utils
|
AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
--> AIR301_names.py:114:1
--> AIR301_names.py:113:1
|
113 | # airflow.www.utils
114 | get_sensitive_variables_fields
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115 | should_hide_value_for_key
114 | should_hide_value_for_key
|
AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
--> AIR301_names.py:115:1
--> AIR301_names.py:114:1
|
113 | # airflow.www.utils
114 | get_sensitive_variables_fields
115 | should_hide_value_for_key
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
114 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -4,8 +4,6 @@ use ruff_python_ast::{
self as ast, Expr, ExprBinOp, ExprContext, ExprNoneLiteral, Operator, PythonVersion,
helpers::{pep_604_union, typing_optional},
name::Name,
operator_precedence::OperatorPrecedence,
parenthesize::parenthesized_range,
};
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
use ruff_text_size::{Ranged, TextRange};
@@ -240,19 +238,7 @@ fn create_fix(
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
// Check if we need parentheses to preserve operator precedence
let content = if needs_parentheses_for_precedence(
semantic,
literal_expr,
checker.comment_ranges(),
checker.source(),
) {
format!("({})", checker.generator().expr(&union_expr))
} else {
checker.generator().expr(&union_expr)
};
let content = checker.generator().expr(&union_expr);
let union_edit = Edit::range_replacement(content, literal_expr.range());
Fix::applicable_edit(union_edit, applicability)
}
@@ -270,37 +256,3 @@ enum UnionKind {
TypingOptional,
BitOr,
}
/// Check if the union expression needs parentheses to preserve operator precedence.
/// This is needed when the union is part of a larger expression where the `|` operator
/// has lower precedence than the surrounding operations (like attribute access).
fn needs_parentheses_for_precedence(
semantic: &ruff_python_semantic::SemanticModel,
literal_expr: &Expr,
comment_ranges: &ruff_python_trivia::CommentRanges,
source: &str,
) -> bool {
// Get the parent expression to check if we're in a context that needs parentheses
let Some(parent_expr) = semantic.current_expression_parent() else {
return false;
};
// Check if the literal expression is already parenthesized
if parenthesized_range(
literal_expr.into(),
parent_expr.into(),
comment_ranges,
source,
)
.is_some()
{
return false; // Already parenthesized, don't add more
}
// Check if the parent expression has higher precedence than the `|` operator
let union_precedence = OperatorPrecedence::BitOr;
let parent_precedence = OperatorPrecedence::from(parent_expr);
// If the parent operation has higher precedence than `|`, we need parentheses
parent_precedence > union_precedence
}

View File

@@ -423,117 +423,5 @@ PYI061 Use `None` rather than `Literal[None]`
79 | d: None | (Literal[None] | None)
80 | e: None | ((None | Literal[None]) | None) | None
| ^^^^
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
|
help: Replace with `None`
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:83:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
| ^^^^
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
|
help: Replace with `Literal[...] | None`
80 | e: None | ((None | Literal[None]) | None) | None
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
- print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
83 + print((Literal[1] | None).__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:84:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
| ^^^^
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
|
help: Replace with `Literal[...] | None`
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
- print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
84 + print((Literal[1] | None).method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:85:18
|
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
| ^^^^
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
|
help: Replace with `Literal[...] | None`
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
- print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
85 + print((Literal[1] | None)[0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:86:18
|
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
| ^^^^
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Literal[...] | None`
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
- print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
86 + print((Literal[1] | None) + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:87:18
|
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
| ^^^^
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Literal[...] | None`
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
- print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
87 + print((Literal[1] | None) * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:88:19
|
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
| ^^^^
|
help: Replace with `Literal[...] | None`
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
- print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
88 + print((Literal[1] | None).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -465,153 +465,5 @@ PYI061 Use `None` rather than `Literal[None]`
79 | d: None | (Literal[None] | None)
80 | e: None | ((None | Literal[None]) | None) | None
| ^^^^
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
|
help: Replace with `None`
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:83:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
| ^^^^
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
80 | e: None | ((None | Literal[None]) | None) | None
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
- print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
83 + print(Optional[Literal[1]].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:84:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
| ^^^^
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
- print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
84 + print(Optional[Literal[1]].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:85:18
|
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
| ^^^^
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
- print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
85 + print(Optional[Literal[1]][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:86:18
|
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
| ^^^^
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
- print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
86 + print(Optional[Literal[1]] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:87:18
|
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
| ^^^^
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
- print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
87 + print(Optional[Literal[1]] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:88:19
|
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
| ^^^^
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
- print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
88 + print((Optional[Literal[1]]).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -28,7 +28,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_google.py"))]
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_google.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_google.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_google.py"))]
@@ -51,7 +50,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_numpy.py"))]
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_numpy.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_numpy.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_numpy.py"))]

View File

@@ -1,14 +1,14 @@
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, Stmt, visitor};
use ruff_python_semantic::analyze::{function_type, visibility};
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_source_file::NewlineWithTrailingNewline;
use ruff_text_size::{Ranged, TextRange};
use crate::Violation;
use crate::checkers::ast::Checker;
@@ -18,62 +18,6 @@ use crate::docstrings::styles::SectionStyle;
use crate::registry::Rule;
use crate::rules::pydocstyle::settings::Convention;
/// ## What it does
/// Checks for function docstrings that include parameters which are not
/// in the function signature.
///
/// ## Why is this bad?
/// If a docstring documents a parameter which is not in the function signature,
/// it can be misleading to users and/or a sign of incomplete documentation or
/// refactors.
///
/// ## Example
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
/// Args:
/// distance: Distance traveled.
/// time: Time spent traveling.
/// acceleration: Rate of change of speed.
///
/// Returns:
/// Speed as distance divided by time.
/// """
/// return distance / time
/// ```
///
/// Use instead:
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
/// Args:
/// distance: Distance traveled.
/// time: Time spent traveling.
///
/// Returns:
/// Speed as distance divided by time.
/// """
/// return distance / time
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct DocstringExtraneousParameter {
id: String,
}
impl Violation for DocstringExtraneousParameter {
#[derive_message_formats]
fn message(&self) -> String {
let DocstringExtraneousParameter { id } = self;
format!("Documented parameter `{id}` is not in the function's signature")
}
fn fix_title(&self) -> Option<String> {
Some("Remove the extraneous parameter from the docstring".to_string())
}
}
/// ## What it does
/// Checks for functions with `return` statements that do not have "Returns"
/// sections in their docstrings.
@@ -452,19 +396,6 @@ impl GenericSection {
}
}
/// A parameter in a docstring with its text range.
#[derive(Debug, Clone)]
struct ParameterEntry<'a> {
name: &'a str,
range: TextRange,
}
impl Ranged for ParameterEntry<'_> {
fn range(&self) -> TextRange {
self.range
}
}
/// A "Raises" section in a docstring.
#[derive(Debug)]
struct RaisesSection<'a> {
@@ -483,46 +414,17 @@ impl<'a> RaisesSection<'a> {
/// a "Raises" section.
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
Self {
raised_exceptions: parse_raises(section.following_lines_str(), style),
raised_exceptions: parse_entries(section.following_lines_str(), style),
range: section.range(),
}
}
}
/// An "Args" or "Parameters" section in a docstring.
#[derive(Debug)]
struct ParametersSection<'a> {
parameters: Vec<ParameterEntry<'a>>,
range: TextRange,
}
impl Ranged for ParametersSection<'_> {
fn range(&self) -> TextRange {
self.range
}
}
impl<'a> ParametersSection<'a> {
/// Return the parameters for the docstring, or `None` if the docstring does not contain
/// an "Args" or "Parameters" section.
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
Self {
parameters: parse_parameters(
section.following_lines_str(),
section.following_range().start(),
style,
),
range: section.section_name_range(),
}
}
}
#[derive(Debug, Default)]
struct DocstringSections<'a> {
returns: Option<GenericSection>,
yields: Option<GenericSection>,
raises: Option<RaisesSection<'a>>,
parameters: Option<ParametersSection<'a>>,
}
impl<'a> DocstringSections<'a> {
@@ -530,10 +432,6 @@ impl<'a> DocstringSections<'a> {
let mut docstring_sections = Self::default();
for section in sections {
match section.kind() {
SectionKind::Args | SectionKind::Arguments | SectionKind::Parameters => {
docstring_sections.parameters =
Some(ParametersSection::from_section(&section, style));
}
SectionKind::Raises => {
docstring_sections.raises = Some(RaisesSection::from_section(&section, style));
}
@@ -550,142 +448,18 @@ impl<'a> DocstringSections<'a> {
}
}
/// Parse the entries in a "Parameters" section of a docstring.
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_parameters(
content: &str,
content_start: TextSize,
style: Option<SectionStyle>,
) -> Vec<ParameterEntry<'_>> {
match style {
Some(SectionStyle::Google) => parse_parameters_google(content, content_start),
Some(SectionStyle::Numpy) => parse_parameters_numpy(content, content_start),
None => {
let entries = parse_parameters_google(content, content_start);
if entries.is_empty() {
parse_parameters_numpy(content, content_start)
} else {
entries
}
}
}
}
/// Parses Google-style "Args" sections of the form:
///
/// ```python
/// Args:
/// a (int): The first number to add.
/// b (int): The second number to add.
/// ```
fn parse_parameters_google(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = Vec::new();
// Find first entry to determine indentation
let Some(first_arg) = content.lines().next() else {
return entries;
};
let indentation = &first_arg[..first_arg.len() - first_arg.trim_start().len()];
let mut current_pos = TextSize::ZERO;
for line in content.lines() {
let line_start = current_pos;
current_pos = content.full_line_end(line_start);
if let Some(entry) = line.strip_prefix(indentation) {
if entry
.chars()
.next()
.is_some_and(|first_char| !first_char.is_whitespace())
{
let Some((before_colon, _)) = entry.split_once(':') else {
continue;
};
if let Some(param) = before_colon.split_whitespace().next() {
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
}
}
entries
}
/// Parses NumPy-style "Parameters" sections of the form:
///
/// ```python
/// Parameters
/// ----------
/// a : int
/// The first number to add.
/// b : int
/// The second number to add.
/// ```
fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {
return entries;
};
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
let mut current_pos = content.full_line_end(dashes.text_len());
for potential in lines {
let line_start = current_pos;
current_pos = content.full_line_end(line_start);
if let Some(entry) = potential.strip_prefix(indentation) {
if entry
.chars()
.next()
.is_some_and(|first_char| !first_char.is_whitespace())
{
if let Some(before_colon) = entry.split(':').next() {
let param = before_colon.trim_end();
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
}
}
entries
}
/// Parse the entries in a "Raises" section of a docstring.
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
match style {
Some(SectionStyle::Google) => parse_raises_google(content),
Some(SectionStyle::Numpy) => parse_raises_numpy(content),
Some(SectionStyle::Google) => parse_entries_google(content),
Some(SectionStyle::Numpy) => parse_entries_numpy(content),
None => {
let entries = parse_raises_google(content);
let entries = parse_entries_google(content);
if entries.is_empty() {
parse_raises_numpy(content)
parse_entries_numpy(content)
} else {
entries
}
@@ -693,14 +467,14 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
}
}
/// Parses Google-style "Raises" section of the form:
/// Parses Google-style docstring sections of the form:
///
/// ```python
/// Raises:
/// FasterThanLightError: If speed is greater than the speed of light.
/// DivisionByZero: If attempting to divide by zero.
/// ```
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_entries_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
for potential in content.lines() {
let Some(colon_idx) = potential.find(':') else {
@@ -712,7 +486,7 @@ fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
entries
}
/// Parses NumPy-style "Raises" section of the form:
/// Parses NumPy-style docstring sections of the form:
///
/// ```python
/// Raises
@@ -722,7 +496,7 @@ fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
/// DivisionByZero
/// If attempting to divide by zero.
/// ```
fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {
@@ -1093,17 +867,6 @@ fn is_generator_function_annotated_as_returning_none(
.is_some_and(GeneratorOrIteratorArguments::indicates_none_returned)
}
fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec<&'a str> {
let mut parameters = Vec::new();
let Some(function) = docstring.definition.as_function_def() else {
return parameters;
};
for param in &function.parameters {
parameters.push(param.name());
}
parameters
}
fn is_one_line(docstring: &Docstring) -> bool {
let mut non_empty_line_count = 0;
for line in NewlineWithTrailingNewline::from(docstring.body().as_str()) {
@@ -1117,7 +880,7 @@ fn is_one_line(docstring: &Docstring) -> bool {
true
}
/// DOC102, DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
/// DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
pub(crate) fn check_docstring(
checker: &Checker,
definition: &Definition,
@@ -1157,8 +920,6 @@ pub(crate) fn check_docstring(
visitor.finish()
};
let signature_parameters = parameters_from_signature(docstring);
// DOC201
if checker.is_rule_enabled(Rule::DocstringMissingReturns) {
if should_document_returns(function_def)
@@ -1247,25 +1008,6 @@ pub(crate) fn check_docstring(
}
}
// DOC102
if checker.is_rule_enabled(Rule::DocstringExtraneousParameter) {
// Don't report extraneous parameters if the signature defines *args or **kwargs
if function_def.parameters.vararg.is_none() && function_def.parameters.kwarg.is_none() {
if let Some(docstring_params) = docstring_sections.parameters {
for docstring_param in &docstring_params.parameters {
if !signature_parameters.contains(&docstring_param.name) {
checker.report_diagnostic(
DocstringExtraneousParameter {
id: docstring_param.name.to_string(),
},
docstring_param.range(),
);
}
}
}
}
}
// Avoid applying "extraneous" rules to abstract methods. An abstract method's docstring _could_
// document that it raises an exception without including the exception in the implementation.
if !visibility::is_abstract(&function_def.decorator_list, semantic) {

View File

@@ -1,180 +0,0 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_google.py:7:9
|
6 | Args:
7 | a (int): The first number to add.
| ^
8 | b (int): The second number to add.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `multiplier` is not in the function's signature
--> DOC102_google.py:23:9
|
21 | Args:
22 | lst (list of int): A list of integers.
23 | multiplier (int): The multiplier for each element in the list.
| ^^^^^^^^^^
24 |
25 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `numbers` is not in the function's signature
--> DOC102_google.py:37:9
|
36 | Args:
37 | numbers (list of int): A list of integers to search through.
| ^^^^^^^
38 |
39 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `name` is not in the function's signature
--> DOC102_google.py:51:9
|
50 | Args:
51 | name (str): The name of the user.
| ^^^^
52 | age (int): The age of the user.
53 | email (str): The user's email address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `age` is not in the function's signature
--> DOC102_google.py:52:9
|
50 | Args:
51 | name (str): The name of the user.
52 | age (int): The age of the user.
| ^^^
53 | email (str): The user's email address.
54 | location (str): The location of the user.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `email` is not in the function's signature
--> DOC102_google.py:53:9
|
51 | name (str): The name of the user.
52 | age (int): The age of the user.
53 | email (str): The user's email address.
| ^^^^^
54 | location (str): The location of the user.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `tax_rate` is not in the function's signature
--> DOC102_google.py:74:9
|
72 | Args:
73 | item_prices (list of float): A list of prices for each item.
74 | tax_rate (float): The tax rate to apply.
| ^^^^^^^^
75 | discount (float): The discount to subtract from the total.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `to_address` is not in the function's signature
--> DOC102_google.py:94:9
|
92 | subject (str): The subject of the email.
93 | body (str): The content of the email.
94 | to_address (str): The recipient's email address.
| ^^^^^^^^^^
95 | cc_address (str, optional): The email address for CC. Defaults to None.
96 | bcc_address (str, optional): The email address for BCC. Defaults to None.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `cc_address` is not in the function's signature
--> DOC102_google.py:95:9
|
93 | body (str): The content of the email.
94 | to_address (str): The recipient's email address.
95 | cc_address (str, optional): The email address for CC. Defaults to None.
| ^^^^^^^^^^
96 | bcc_address (str, optional): The email address for BCC. Defaults to None.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `items` is not in the function's signature
--> DOC102_google.py:126:9
|
124 | Args:
125 | order_id (int): The unique identifier for the order.
126 | *items (str): Variable length argument list of items in the order.
| ^^^^^^
127 | **details (dict): Additional details such as shipping method and address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `details` is not in the function's signature
--> DOC102_google.py:127:9
|
125 | order_id (int): The unique identifier for the order.
126 | *items (str): Variable length argument list of items in the order.
127 | **details (dict): Additional details such as shipping method and address.
| ^^^^^^^^^
128 |
129 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value` is not in the function's signature
--> DOC102_google.py:150:13
|
149 | Args:
150 | value (int, optional): The initial value of the calculator. Defaults to 0.
| ^^^^^
151 | """
152 | self.value = value
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_google.py:160:13
|
159 | Args:
160 | number (int or float): The number to add to the current value.
| ^^^^^^
161 |
162 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value_str` is not in the function's signature
--> DOC102_google.py:175:13
|
174 | Args:
175 | value_str (str): The string representing the initial value.
| ^^^^^^^^^
176 |
177 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_google.py:190:13
|
189 | Args:
190 | number (any): The value to check.
| ^^^^^^
191 |
192 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_google.py:258:9
|
257 | Args:
258 | a: The first number to add.
| ^
259 | b: The second number to add.
|
help: Remove the extraneous parameter from the docstring

View File

@@ -1,189 +0,0 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_numpy.py:8:5
|
6 | Parameters
7 | ----------
8 | a : int
| ^
9 | The first number to add.
10 | b : int
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `multiplier` is not in the function's signature
--> DOC102_numpy.py:30:5
|
28 | lst : list of int
29 | A list of integers.
30 | multiplier : int
| ^^^^^^^^^^
31 | The multiplier for each element in the list.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `numbers` is not in the function's signature
--> DOC102_numpy.py:48:5
|
46 | Parameters
47 | ----------
48 | numbers : list of int
| ^^^^^^^
49 | A list of integers to search through.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `name` is not in the function's signature
--> DOC102_numpy.py:66:5
|
64 | Parameters
65 | ----------
66 | name : str
| ^^^^
67 | The name of the user.
68 | age : int
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `age` is not in the function's signature
--> DOC102_numpy.py:68:5
|
66 | name : str
67 | The name of the user.
68 | age : int
| ^^^
69 | The age of the user.
70 | email : str
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `email` is not in the function's signature
--> DOC102_numpy.py:70:5
|
68 | age : int
69 | The age of the user.
70 | email : str
| ^^^^^
71 | The user's email address.
72 | location : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `tax_rate` is not in the function's signature
--> DOC102_numpy.py:97:5
|
95 | item_prices : list of float
96 | A list of prices for each item.
97 | tax_rate : float
| ^^^^^^^^
98 | The tax rate to apply.
99 | discount : float
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `to_address` is not in the function's signature
--> DOC102_numpy.py:124:5
|
122 | body : str
123 | The content of the email.
124 | to_address : str
| ^^^^^^^^^^
125 | The recipient's email address.
126 | cc_address : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `cc_address` is not in the function's signature
--> DOC102_numpy.py:126:5
|
124 | to_address : str
125 | The recipient's email address.
126 | cc_address : str, optional
| ^^^^^^^^^^
127 | The email address for CC, by default None.
128 | bcc_address : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `items` is not in the function's signature
--> DOC102_numpy.py:168:5
|
166 | order_id : int
167 | The unique identifier for the order.
168 | *items : str
| ^^^^^^
169 | Variable length argument list of items in the order.
170 | **details : dict
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `details` is not in the function's signature
--> DOC102_numpy.py:170:5
|
168 | *items : str
169 | Variable length argument list of items in the order.
170 | **details : dict
| ^^^^^^^^^
171 | Additional details such as shipping method and address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value` is not in the function's signature
--> DOC102_numpy.py:197:9
|
195 | Parameters
196 | ----------
197 | value : int, optional
| ^^^^^
198 | The initial value of the calculator, by default 0.
199 | """
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_numpy.py:209:9
|
207 | Parameters
208 | ----------
209 | number : int or float
| ^^^^^^
210 | The first number to add.
211 | number2 : int or float
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value_str` is not in the function's signature
--> DOC102_numpy.py:230:9
|
228 | Parameters
229 | ----------
230 | value_str : str
| ^^^^^^^^^
231 | The string representing the initial value.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_numpy.py:249:9
|
247 | Parameters
248 | ----------
249 | number : any
| ^^^^^^
250 | The value to check.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_numpy.py:300:5
|
298 | Parameters
299 | ----------
300 | a
| ^
301 | The first number to add.
302 | b
|
help: Remove the extraneous parameter from the docstring

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use crate::Violation;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## What it does
/// Checks for `continue` statements outside of loops.
@@ -26,3 +29,28 @@ impl Violation for ContinueOutsideLoop {
"`continue` not properly in loop".to_string()
}
}
/// F702
pub(crate) fn continue_outside_loop<'a>(
checker: &Checker,
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) {
let mut child = stmt;
for parent in parents {
match parent {
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
checker.report_diagnostic(ContinueOutsideLoop, stmt.range());
}

View File

@@ -19,7 +19,7 @@ mod tests {
use crate::rules::{isort, pyupgrade};
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_snippet};
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
use crate::{assert_diagnostics, settings};
#[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))]
#[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))]
@@ -140,28 +140,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))]
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
fn type_var_default_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview_diff", path.to_string_lossy());
assert_diagnostics_diff!(
snapshot,
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Disabled,
..settings::LinterSettings::for_rule(rule_code)
},
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
);
Ok(())
}
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]

View File

@@ -14,14 +14,13 @@ use ruff_python_ast::{
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_type_var_default_enabled;
pub(crate) use non_pep695_generic_class::*;
pub(crate) use non_pep695_generic_function::*;
pub(crate) use non_pep695_type_alias::*;
pub(crate) use private_type_parameter::*;
use crate::checkers::ast::Checker;
mod non_pep695_generic_class;
mod non_pep695_generic_function;
mod non_pep695_type_alias;
@@ -123,10 +122,6 @@ impl Display for DisplayTypeVar<'_> {
}
}
}
if let Some(default) = self.type_var.default {
f.write_str(" = ")?;
f.write_str(&self.source[default.range()])?;
}
Ok(())
}
@@ -138,63 +133,66 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam {
name,
restriction,
kind,
default,
default: _, // TODO(brent) see below
}: &'a TypeVar<'a>,
) -> Self {
let default = default.map(|expr| Box::new(expr.clone()));
match kind {
TypeParamKind::TypeVar => TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())),
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: constraints.iter().map(|expr| (*expr).clone()).collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
Some(TypeVarRestriction::AnyStr) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: vec![
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("str"),
ctx: ast::ExprContext::Load,
}),
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("bytes"),
ctx: ast::ExprContext::Load,
}),
],
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,
},
default,
}),
TypeParamKind::TypeVar => {
TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())),
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: constraints.iter().map(|expr| (*expr).clone()).collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
Some(TypeVarRestriction::AnyStr) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: vec![
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("str"),
ctx: ast::ExprContext::Load,
}),
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("bytes"),
ctx: ast::ExprContext::Load,
}),
],
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,
},
// We don't handle defaults here yet. Should perhaps be a different rule since
// defaults are only valid in 3.13+.
default: None,
})
}
TypeParamKind::TypeVarTuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
default,
default: None,
}),
TypeParamKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
default,
default: None,
}),
}
}
@@ -320,8 +318,8 @@ pub(crate) fn expr_name_to_type_var<'a>(
.first()
.is_some_and(Expr::is_string_literal_expr)
{
// `default` was added in PEP 696 and Python 3.13. We now support converting
// TypeVars with defaults to PEP 695 type parameters.
// TODO(brent) `default` was added in PEP 696 and Python 3.13 but can't be used in
// generic type parameters before that
//
// ```python
// T = TypeVar("T", default=Any, bound=str)
@@ -369,22 +367,21 @@ fn in_nested_context(checker: &Checker) -> bool {
}
/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found.
/// Also returns `None` if any `TypeVar` has a default value and preview mode is not enabled.
fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<TypeVar<'a>>> {
fn check_type_vars(vars: Vec<TypeVar<'_>>) -> Option<Vec<TypeVar<'_>>> {
if vars.is_empty() {
return None;
}
// If any type variables have defaults and preview mode is not enabled, skip the rule
if vars.iter().any(|tv| tv.default.is_some())
&& !is_type_var_default_enabled(checker.settings())
{
return None;
}
// If any type variables were not unique, just bail out here. this is a runtime error and we
// can't predict what the user wanted.
(vars.iter().unique_by(|tvar| tvar.name).count() == vars.len()).then_some(vars)
// can't predict what the user wanted. also bail out if any Python 3.13+ default values are
// found on the type parameters
(vars
.iter()
.unique_by(|tvar| tvar.name)
.filter(|tvar| tvar.default.is_none())
.count()
== vars.len())
.then_some(vars)
}
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if

View File

@@ -186,7 +186,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD
//
// just because we can't confirm that `SomethingElse` is a `TypeVar`
if !visitor.any_skipped {
let Some(type_vars) = check_type_vars(visitor.vars, checker) else {
let Some(type_vars) = check_type_vars(visitor.vars) else {
diagnostic.defuse();
return;
};

View File

@@ -154,7 +154,7 @@ pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &Stmt
}
}
let Some(type_vars) = check_type_vars(type_vars, checker) else {
let Some(type_vars) = check_type_vars(type_vars) else {
return;
};

View File

@@ -8,7 +8,6 @@ use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssi
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_type_var_default_enabled;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
@@ -233,10 +232,8 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
.unique_by(|tvar| tvar.name)
.collect::<Vec<_>>();
// Skip if any TypeVar has defaults and preview mode is not enabled
if vars.iter().any(|tv| tv.default.is_some())
&& !is_type_var_default_enabled(checker.settings())
{
// TODO(brent) handle `default` arg for Python 3.13+
if vars.iter().any(|tv| tv.default.is_some()) {
return;
}

View File

@@ -217,7 +217,7 @@ UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keywo
44 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45 |
46 | # `default` was added in Python 3.13
46 | # `default` should be skipped for now, added in Python 3.13
|
help: Use the `type` keyword
41 |
@@ -226,7 +226,7 @@ help: Use the `type` keyword
- x: typing.TypeAlias = list[T]
44 + type x = list[T]
45 |
46 | # `default` was added in Python 3.13
46 | # `default` should be skipped for now, added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
note: This is an unsafe fix and may change runtime behavior
@@ -355,26 +355,6 @@ help: Use the `type` keyword
87 | # OK: Other name
88 | T = TypeVar("T", bound=SupportGt)
UP040 [*] Type alias `AnyList` uses `TypeAliasType` assignment instead of the `type` keyword
--> UP040.py:95:1
|
93 | # `default` was added in Python 3.13
94 | T = typing.TypeVar("T", default=Any)
95 | AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96 |
97 | # unsafe fix if comments within the fix
|
help: Use the `type` keyword
92 |
93 | # `default` was added in Python 3.13
94 | T = typing.TypeVar("T", default=Any)
- AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
95 + type AnyList[T = Any] = list[T]
96 |
97 | # unsafe fix if comments within the fix
98 | T = TypeVar("T")
UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword
--> UP040.py:99:1
|
@@ -489,8 +469,6 @@ UP040 [*] Type alias `T` uses `TypeAlias` annotation instead of the `type` keywo
129 | | # comment7
130 | | ) # comment8
| |_^
131 |
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
|
help: Use the `type` keyword
119 | | str

View File

@@ -1,49 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:48:1
|
46 | # `default` was added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
48 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | # OK
|
help: Use the `type` keyword
45 |
46 | # `default` was added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
- x: typing.TypeAlias = list[T]
48 + type x[T = Any] = list[T]
49 |
50 | # OK
51 | x: TypeAlias
note: This is an unsafe fix and may change runtime behavior
UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:134:1
|
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
133 | T_default = TypeVar("T_default", default=int)
134 | DefaultList: TypeAlias = list[T_default]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use the `type` keyword
131 |
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
133 | T_default = TypeVar("T_default", default=int)
- DefaultList: TypeAlias = list[T_default]
134 + type DefaultList[T_default = int] = list[T_default]
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,10 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -1,48 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
UP046 [*] Generic class `DefaultTypeVar` uses `Generic` subclass instead of type parameters
--> UP046_0.py:129:22
|
129 | class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
| ^^^^^^^^^^
130 | var: V
|
help: Use type parameters
126 | V = TypeVar("V", default=Any, bound=str)
127 |
128 |
- class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
129 + class DefaultTypeVar[V: str = Any]: # -> [V: str = Any]
130 | var: V
131 |
132 |
note: This is an unsafe fix and may change runtime behavior
UP046 [*] Generic class `DefaultOnlyTypeVar` uses `Generic` subclass instead of type parameters
--> UP046_0.py:137:26
|
137 | class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
| ^^^^^^^^^^
138 | var: W
|
help: Use type parameters
134 | W = TypeVar("W", default=int)
135 |
136 |
- class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
137 + class DefaultOnlyTypeVar[W = int]: # -> [W = int]
138 | var: W
139 |
140 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,10 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -1,29 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 1
--- Added ---
UP047 [*] Generic function `default_var` should use type parameters
--> UP047.py:51:5
|
51 | def default_var(v: V) -> V:
| ^^^^^^^^^^^^^^^^^
52 | return v
|
help: Use type parameters
48 | V = TypeVar("V", default=Any, bound=str)
49 |
50 |
- def default_var(v: V) -> V:
51 + def default_var[V: str = Any](v: V) -> V:
52 | return v
53 |
54 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -26,7 +26,7 @@ use ruff_source_file::SourceFileBuilder;
use crate::codes::Rule;
use crate::fix::{FixResult, fix_file};
use crate::linter::check_path;
use crate::message::EmitterContext;
use crate::message::{EmitterContext, create_syntax_error_diagnostic};
use crate::package::PackageRoot;
use crate::packaging::detect_package_root;
use crate::settings::types::UnsafeFixes;
@@ -405,7 +405,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
diagnostic
})
.chain(parsed.errors().iter().map(|parse_error| {
Diagnostic::invalid_syntax(source_code.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error)
}))
.sorted_by(Diagnostic::ruff_start_ordering)
.collect();
@@ -419,7 +419,7 @@ fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind)
let messages: Vec<_> = errors
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.collect();

View File

@@ -706,6 +706,8 @@ f'{1:hy "user"}'
f'{1: abcd "{1}" }'
f'{1: abcd "{'aa'}" }'
f'{1=: "abcd {'aa'}}'
# FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
# spec, which is valid even before 3.12.
f'{x:a{z:hy "user"}} \'\'\''
# Changing the outer quotes is fine because the format-spec is in a nested expression.
@@ -748,7 +750,3 @@ print(f"{ # Tuple with multiple elements that doesn't fit on a single line gets
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1, }")
# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"

View File

@@ -144,12 +144,6 @@ pub(crate) enum InterpolatedStringState {
///
/// The containing `FStringContext` is the surrounding f-string context.
InsideInterpolatedElement(InterpolatedStringContext),
/// The formatter is inside more than one nested f-string, such as in `nested` in:
///
/// ```py
/// f"{f'''{'nested'} inner'''} outer"
/// ```
NestedInterpolatedElement(InterpolatedStringContext),
/// The formatter is outside an f-string.
#[default]
Outside,
@@ -158,18 +152,12 @@ pub(crate) enum InterpolatedStringState {
impl InterpolatedStringState {
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
match self {
InterpolatedStringState::InsideInterpolatedElement(context)
| InterpolatedStringState::NestedInterpolatedElement(context) => {
InterpolatedStringState::InsideInterpolatedElement(context) => {
Some(context.is_multiline())
}
InterpolatedStringState::Outside => None,
}
}
/// Returns `true` if the interpolated string state is [`NestedInterpolatedElement`].
pub(crate) fn is_nested(self) -> bool {
matches!(self, Self::NestedInterpolatedElement(..))
}
}
/// The position of a top-level statement in the module.

View File

@@ -181,16 +181,10 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
let item = format_with(|f: &mut PyFormatter| {
// Update the context to be inside the f-string expression element.
let state = match f.context().interpolated_string_state() {
InterpolatedStringState::InsideInterpolatedElement(_)
| InterpolatedStringState::NestedInterpolatedElement(_) => {
InterpolatedStringState::NestedInterpolatedElement(self.context)
}
InterpolatedStringState::Outside => {
InterpolatedStringState::InsideInterpolatedElement(self.context)
}
};
let f = &mut WithInterpolatedStringState::new(state, f);
let f = &mut WithInterpolatedStringState::new(
InterpolatedStringState::InsideInterpolatedElement(self.context),
f,
);
write!(f, [bracket_spacing, expression.format()])?;

View File

@@ -46,15 +46,8 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
.unwrap_or(self.context.options().quote_style());
let supports_pep_701 = self.context.options().target_version().supports_pep_701();
// Preserve the existing quote style for nested interpolations more than one layer deep, if
// PEP 701 isn't supported.
if !supports_pep_701 && self.context.interpolated_string_state().is_nested() {
return QuoteStyle::Preserve;
}
// For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't.
if let InterpolatedStringState::InsideInterpolatedElement(parent_context)
| InterpolatedStringState::NestedInterpolatedElement(parent_context) =
if let InterpolatedStringState::InsideInterpolatedElement(parent_context) =
self.context.interpolated_string_state()
{
let parent_flags = parent_context.flags();

View File

@@ -28,11 +28,12 @@ but none started with prefix {parentdir_prefix}"
f'{{NOT \'a\' "formatted" "value"}}'
f"some f-string with {a} {few():.2f} {formatted.values!r}"
-f'some f-string with {a} {few(""):.2f} {formatted.values!r}'
+f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
f"{f'''{'nested'} inner'''} outer"
-f"{f'''{'nested'} inner'''} outer"
-f"\"{f'{nested} inner'}\" outer"
-f"space between opening braces: { {a for a in (1, 2, 3)}}"
-f'Hello \'{tricky + "example"}\''
+f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
+f"{f'''{"nested"} inner'''} outer"
+f'"{f"{nested} inner"}" outer'
+f"space between opening braces: { {a for a in (1, 2, 3)} }"
+f"Hello '{tricky + 'example'}'"
@@ -48,7 +49,7 @@ f"{{NOT a formatted value}}"
f'{{NOT \'a\' "formatted" "value"}}'
f"some f-string with {a} {few():.2f} {formatted.values!r}"
f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
f"{f'''{'nested'} inner'''} outer"
f"{f'''{"nested"} inner'''} outer"
f'"{f"{nested} inner"}" outer'
f"space between opening braces: { {a for a in (1, 2, 3)} }"
f"Hello '{tricky + 'example'}'"
@@ -71,3 +72,17 @@ f'Hello \'{tricky + "example"}\''
f"Tried directories {str(rootdirs)} \
but none started with prefix {parentdir_prefix}"
```
## New Unsupported Syntax Errors
error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:6:9
|
4 | f"some f-string with {a} {few():.2f} {formatted.values!r}"
5 | f"some f-string with {a} {few(''):.2f} {formatted.values!r}"
6 | f"{f'''{"nested"} inner'''} outer"
| ^
7 | f'"{f"{nested} inner"}" outer'
8 | f"space between opening braces: { {a for a in (1, 2, 3)} }"
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.

View File

@@ -712,6 +712,8 @@ f'{1:hy "user"}'
f'{1: abcd "{1}" }'
f'{1: abcd "{'aa'}" }'
f'{1=: "abcd {'aa'}}'
# FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
# spec, which is valid even before 3.12.
f'{x:a{z:hy "user"}} \'\'\''
# Changing the outer quotes is fine because the format-spec is in a nested expression.
@@ -754,10 +756,6 @@ print(f"{ # Tuple with multiple elements that doesn't fit on a single line gets
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1, }")
# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
```
## Outputs
@@ -1536,8 +1534,10 @@ f'{f"""other " """}'
f'{1: hy "user"}'
f'{1:hy "user"}'
f'{1: abcd "{1}" }'
f'{1: abcd "{'aa'}" }'
f'{1: abcd "{"aa"}" }'
f'{1=: "abcd {'aa'}}'
# FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
# spec, which is valid even before 3.12.
f"{x:a{z:hy \"user\"}} '''"
# Changing the outer quotes is fine because the format-spec is in a nested expression.
@@ -1585,10 +1585,6 @@ print(
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1 }")
# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
```
@@ -2367,8 +2363,10 @@ f'{f"""other " """}'
f'{1: hy "user"}'
f'{1:hy "user"}'
f'{1: abcd "{1}" }'
f'{1: abcd "{'aa'}" }'
f'{1: abcd "{"aa"}" }'
f'{1=: "abcd {'aa'}}'
# FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
# spec, which is valid even before 3.12.
f"{x:a{z:hy \"user\"}} '''"
# Changing the outer quotes is fine because the format-spec is in a nested expression.
@@ -2416,14 +2414,34 @@ print(
# Regression tests for https://github.com/astral-sh/ruff/issues/15536
print(f"{ {}, 1 }")
# The inner quotes should not be changed to double quotes before Python 3.12
f"{f'''{'nested'} inner'''} outer"
```
### Unsupported Syntax Errors
error[invalid-syntax]: Cannot use an escape sequence (backslash) in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:764:19
|
762 | # FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
763 | # spec, which is valid even before 3.12.
764 | f"{x:a{z:hy \"user\"}} '''"
| ^
765 |
766 | # Changing the outer quotes is fine because the format-spec is in a nested expression.
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
error[invalid-syntax]: Cannot use an escape sequence (backslash) in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:764:13
|
762 | # FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
763 | # spec, which is valid even before 3.12.
764 | f"{x:a{z:hy \"user\"}} '''"
| ^
765 |
766 | # Changing the outer quotes is fine because the format-spec is in a nested expression.
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:178:8
|
@@ -2434,3 +2452,27 @@ error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python
179 | f"foo {'"bar"'}"
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:773:14
|
771 | f'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error
772 | f"{1=: abcd \'\'}" # Changing the quotes here is fine because the inner quotes aren't the opposite quotes
773 | f"{1=: abcd \"\"}" # Changing the quotes here is fine because the inner quotes are escaped
| ^
774 | # Don't change the quotes in the following cases:
775 | f'{x=:hy "user"} \'\'\''
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.
error[invalid-syntax]: Cannot reuse outer quote character in f-strings on Python 3.10 (syntax was added in Python 3.12)
--> fstring.py:764:14
|
762 | # FIXME(brent) This should not be a syntax error on output. The escaped quotes are in the format
763 | # spec, which is valid even before 3.12.
764 | f"{x:a{z:hy \"user\"}} '''"
| ^
765 |
766 | # Changing the outer quotes is fine because the format-spec is in a nested expression.
|
warning: Only accept new syntax errors if they are also present in the input. The formatter should not introduce syntax errors.

View File

@@ -1,2 +0,0 @@
# parse_options: {"target-version": "3.12"}
f"{1:""}" # this is a ParseError on all versions

View File

@@ -1,2 +0,0 @@
# parse_options: {"target-version": "3.11"}
f"{1:''}" # but this is okay on all versions

View File

@@ -5,5 +5,3 @@ f"""{f'''{f'{"# not a comment"}'}'''}"""
f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
f"escape outside of \t {expr}\n"
f"test\"abcd"
f"{1:\x64}" # escapes are valid in the format spec
f"{1:\"d\"}" # this also means that escaped outer quotes are valid

View File

@@ -1571,8 +1571,6 @@ impl<'src> Parser<'src> {
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
// f"escape outside of \t {expr}\n"
// f"test\"abcd"
// f"{1:\x64}" # escapes are valid in the format spec
// f"{1:\"d\"}" # this also means that escaped outer quotes are valid
// test_err pep701_f_string_py311
// # parse_options: {"target-version": "3.11"}
@@ -1588,13 +1586,6 @@ impl<'src> Parser<'src> {
// f"""{f"""{x}"""}""" # mark the whole triple quote
// f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors
// test_err nested_quote_in_format_spec_py312
// # parse_options: {"target-version": "3.12"}
// f"{1:""}" # this is a ParseError on all versions
// test_ok non_nested_quote_in_format_spec_py311
// # parse_options: {"target-version": "3.11"}
// f"{1:''}" # but this is okay on all versions
let range = self.node_range(start);
if !self.options.target_version.supports_pep_701()
@@ -1603,29 +1594,22 @@ impl<'src> Parser<'src> {
let quote_bytes = flags.quote_str().as_bytes();
let quote_len = flags.quote_len();
for expr in elements.interpolations() {
// We need to check the whole expression range, including any leading or trailing
// debug text, but exclude the format spec, where escapes and escaped, reused quotes
// are allowed.
let range = expr
.format_spec
.as_ref()
.map(|format_spec| TextRange::new(expr.start(), format_spec.start()))
.unwrap_or(expr.range);
for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) {
for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes())
{
let slash_position = TextSize::try_from(slash_position).unwrap();
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
TextRange::at(range.start() + slash_position, '\\'.text_len()),
TextRange::at(expr.range.start() + slash_position, '\\'.text_len()),
);
}
if let Some(quote_position) =
memchr::memmem::find(self.source[range].as_bytes(), quote_bytes)
memchr::memmem::find(self.source[expr.range].as_bytes(), quote_bytes)
{
let quote_position = TextSize::try_from(quote_position).unwrap();
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote),
TextRange::at(range.start() + quote_position, quote_len),
TextRange::at(expr.range.start() + quote_position, quote_len),
);
}
}

View File

@@ -229,11 +229,6 @@ impl SemanticSyntaxChecker {
Self::add_error(ctx, SemanticSyntaxErrorKind::BreakOutsideLoop, *range);
}
}
Stmt::Continue(ast::StmtContinue { range, .. }) => {
if !ctx.in_loop_context() {
Self::add_error(ctx, SemanticSyntaxErrorKind::ContinueOutsideLoop, *range);
}
}
_ => {}
}
@@ -1136,7 +1131,6 @@ impl Display for SemanticSyntaxError {
write!(f, "Future feature `{name}` is not defined")
}
SemanticSyntaxErrorKind::BreakOutsideLoop => f.write_str("`break` outside loop"),
SemanticSyntaxErrorKind::ContinueOutsideLoop => f.write_str("`continue` outside loop"),
}
}
}
@@ -1513,9 +1507,6 @@ pub enum SemanticSyntaxErrorKind {
/// Represents the use of a `break` statement outside of a loop.
BreakOutsideLoop,
/// Represents the use of a `continue` statement outside of a loop.
ContinueOutsideLoop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]

View File

@@ -1,92 +0,0 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/nested_quote_in_format_spec_py312.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..94,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 44..53,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 44..53,
value: FStringValue {
inner: Concatenated(
[
FString(
FString {
range: 44..50,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 46..49,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 47..48,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 49..49,
node_index: NodeIndex(None),
elements: [],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
Literal(
StringLiteral {
range: 50..53,
node_index: NodeIndex(None),
value: "}",
flags: StringLiteralFlags {
quote_style: Double,
prefix: Empty,
triple_quoted: false,
unclosed: false,
},
},
),
],
),
},
},
),
},
),
],
},
)
```
## Errors
|
1 | # parse_options: {"target-version": "3.12"}
2 | f"{1:""}" # this is a ParseError on all versions
| ^ Syntax Error: f-string: expecting '}'
|

View File

@@ -1,78 +0,0 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/non_nested_quote_in_format_spec_py311.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..90,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 44..53,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 44..53,
value: FStringValue {
inner: Single(
FString(
FString {
range: 44..53,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 46..52,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 47..48,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 49..51,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 49..51,
node_index: NodeIndex(None),
value: "''",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
),
},
},
),
},
),
],
},
)
```

View File

@@ -8,7 +8,7 @@ input_file: crates/ruff_python_parser/resources/inline/ok/pep701_f_string_py311.
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..398,
range: 0..278,
body: [
Expr(
StmtExpr {
@@ -604,130 +604,6 @@ Module(
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 278..289,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 278..289,
value: FStringValue {
inner: Single(
FString(
FString {
range: 278..289,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 280..288,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 281..282,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 283..287,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 283..287,
node_index: NodeIndex(None),
value: "d",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 330..342,
value: FString(
ExprFString {
node_index: NodeIndex(None),
range: 330..342,
value: FStringValue {
inner: Single(
FString(
FString {
range: 330..342,
node_index: NodeIndex(None),
elements: [
Interpolation(
InterpolatedElement {
range: 332..341,
node_index: NodeIndex(None),
expression: NumberLiteral(
ExprNumberLiteral {
node_index: NodeIndex(None),
range: 333..334,
value: Int(
1,
),
},
),
debug_text: None,
conversion: None,
format_spec: Some(
InterpolatedStringFormatSpec {
range: 335..340,
node_index: NodeIndex(None),
elements: [
Literal(
InterpolatedStringLiteralElement {
range: 335..340,
node_index: NodeIndex(None),
value: "\"d\"",
},
),
],
},
),
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
unclosed: false,
},
},
),
),
},
},
),
},
),
],
},
)

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.1"
version = "0.14.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }

132
crates/ty/docs/rules.md generated
View File

@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#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#L116" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L115" target="_blank">View source</a>
</small>
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../rules.md#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#L160" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L159" target="_blank">View source</a>
</small>
@@ -95,7 +95,7 @@ f(int) # error
Default level: <a href="../rules.md#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#L186" 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>
@@ -126,7 +126,7 @@ a = 1
Default level: <a href="../rules.md#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#L211" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L210" target="_blank">View source</a>
</small>
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#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#L237" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L236" target="_blank">View source</a>
</small>
@@ -190,7 +190,7 @@ class B(A): ...
Default level: <a href="../rules.md#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#L302" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L301" target="_blank">View source</a>
</small>
@@ -217,7 +217,7 @@ class B(A, A): ...
Default level: <a href="../rules.md#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#L323" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L322" target="_blank">View source</a>
</small>
@@ -329,7 +329,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../rules.md#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#L527" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L526" target="_blank">View source</a>
</small>
@@ -359,7 +359,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#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#L551" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L550" target="_blank">View source</a>
</small>
@@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../rules.md#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#L355" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L354" target="_blank">View source</a>
</small>
@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#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#L597" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L596" target="_blank">View source</a>
</small>
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#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#L637" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L636" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ a: int = ''
Default level: <a href="../rules.md#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#L1748" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1747" target="_blank">View source</a>
</small>
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#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#L659" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L658" target="_blank">View source</a>
</small>
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#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#L689" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L688" target="_blank">View source</a>
</small>
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#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#L740" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739" target="_blank">View source</a>
</small>
@@ -650,7 +650,7 @@ with 1:
Default level: <a href="../rules.md#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#L761" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760" target="_blank">View source</a>
</small>
@@ -679,7 +679,7 @@ a: str
Default level: <a href="../rules.md#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#L784" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L783" target="_blank">View source</a>
</small>
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: <a href="../rules.md#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#L820" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L819" target="_blank">View source</a>
</small>
@@ -756,7 +756,7 @@ class C[U](Generic[T]): ...
Default level: <a href="../rules.md#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#L572" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L571" target="_blank">View source</a>
</small>
@@ -787,7 +787,7 @@ alice["height"] # KeyError: 'height'
Default level: <a href="../rules.md#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#L846" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L845" target="_blank">View source</a>
</small>
@@ -822,7 +822,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#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#L895" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894" target="_blank">View source</a>
</small>
@@ -856,7 +856,7 @@ class B(metaclass=f): ...
Default level: <a href="../rules.md#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#L501" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L500" target="_blank">View source</a>
</small>
@@ -888,7 +888,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: <a href="../rules.md#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#L922" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L921" target="_blank">View source</a>
</small>
@@ -938,7 +938,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#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#L1021" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1020" target="_blank">View source</a>
</small>
@@ -964,7 +964,7 @@ def f(a: int = ''): ...
Default level: <a href="../rules.md#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#L437" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L436" target="_blank">View source</a>
</small>
@@ -998,7 +998,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#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#L1041" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1040" target="_blank">View source</a>
</small>
@@ -1047,7 +1047,7 @@ def g():
Default level: <a href="../rules.md#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#L618" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L617" target="_blank">View source</a>
</small>
@@ -1072,7 +1072,7 @@ def func() -> int:
Default level: <a href="../rules.md#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#L1084" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1083" target="_blank">View source</a>
</small>
@@ -1130,7 +1130,7 @@ TODO #14889
Default level: <a href="../rules.md#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#L874" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L873" target="_blank">View source</a>
</small>
@@ -1157,7 +1157,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../rules.md#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#L1123" target="_blank">View source</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>
</small>
@@ -1187,7 +1187,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#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#L1147" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1146" target="_blank">View source</a>
</small>
@@ -1217,7 +1217,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#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#L1199" target="_blank">View source</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>
</small>
@@ -1251,7 +1251,7 @@ f(10) # Error
Default level: <a href="../rules.md#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#L1171" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1170" target="_blank">View source</a>
</small>
@@ -1285,7 +1285,7 @@ class C:
Default level: <a href="../rules.md#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#L1227" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1226" target="_blank">View source</a>
</small>
@@ -1320,7 +1320,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#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#L1256" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1255" target="_blank">View source</a>
</small>
@@ -1345,7 +1345,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#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#L1849" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1848" target="_blank">View source</a>
</small>
@@ -1378,7 +1378,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#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#L1275" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1274" target="_blank">View source</a>
</small>
@@ -1407,7 +1407,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#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%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1298" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1297" target="_blank">View source</a>
</small>
@@ -1431,7 +1431,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#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#L1316" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1315" target="_blank">View source</a>
</small>
@@ -1457,7 +1457,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../rules.md#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#L1367" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1366" target="_blank">View source</a>
</small>
@@ -1484,7 +1484,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#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#L1602" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601" target="_blank">View source</a>
</small>
@@ -1542,7 +1542,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#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#L1724" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1723" target="_blank">View source</a>
</small>
@@ -1572,7 +1572,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#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#L1458" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1457" target="_blank">View source</a>
</small>
@@ -1601,7 +1601,7 @@ class B(A): ... # Error raised here
Default level: <a href="../rules.md#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#L1503" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502" target="_blank">View source</a>
</small>
@@ -1628,7 +1628,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#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#L1481" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1480" target="_blank">View source</a>
</small>
@@ -1656,7 +1656,7 @@ def _(x: int):
Default level: <a href="../rules.md#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#L1524" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1523" target="_blank">View source</a>
</small>
@@ -1702,7 +1702,7 @@ class A:
Default level: <a href="../rules.md#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#L1581" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1580" target="_blank">View source</a>
</small>
@@ -1729,7 +1729,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#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#L1623" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1622" target="_blank">View source</a>
</small>
@@ -1757,7 +1757,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#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#L1645" 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>
@@ -1782,7 +1782,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#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#L1664" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663" target="_blank">View source</a>
</small>
@@ -1807,7 +1807,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#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#L1336" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1335" target="_blank">View source</a>
</small>
@@ -1844,7 +1844,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#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#L1683" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1682" target="_blank">View source</a>
</small>
@@ -1872,7 +1872,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#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#L1705" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1704" target="_blank">View source</a>
</small>
@@ -1897,7 +1897,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: <a href="../rules.md#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#L466" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L465" target="_blank">View source</a>
</small>
@@ -1938,7 +1938,7 @@ class SubProto(BaseProto, Protocol):
Default level: <a href="../rules.md#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#L281" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L280" target="_blank">View source</a>
</small>
@@ -1995,7 +1995,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#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#L1388" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1387" target="_blank">View source</a>
</small>
@@ -2023,7 +2023,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../rules.md#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#L134" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L133" target="_blank">View source</a>
</small>
@@ -2055,7 +2055,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#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-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1410" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1409" target="_blank">View source</a>
</small>
@@ -2087,7 +2087,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#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#L1776" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1775" target="_blank">View source</a>
</small>
@@ -2114,7 +2114,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#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#L1563" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1562" target="_blank">View source</a>
</small>
@@ -2169,7 +2169,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
Default level: <a href="../rules.md#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#L1797" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1796" target="_blank">View source</a>
</small>
@@ -2227,7 +2227,7 @@ def g():
Default level: <a href="../rules.md#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#L707" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L706" target="_blank">View source</a>
</small>
@@ -2266,7 +2266,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#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#L965" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L964" target="_blank">View source</a>
</small>
@@ -2329,7 +2329,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../rules.md#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#L263" 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>
@@ -2353,7 +2353,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#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#L1436" 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>

View File

@@ -14,7 +14,7 @@ use anyhow::Result;
use std::sync::Mutex;
use crate::args::{CheckCommand, Command, TerminalColor};
use crate::logging::{VerbosityLevel, setup_tracing};
use crate::logging::setup_tracing;
use crate::printer::Printer;
use anyhow::{Context, anyhow};
use clap::{CommandFactory, Parser};
@@ -128,8 +128,6 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let mut db = ProjectDatabase::new(project_metadata, system)?;
db.project()
.set_verbose(&mut db, verbosity >= VerbosityLevel::Verbose);
if !check_paths.is_empty() {
db.project().set_included_paths(&mut db, check_paths);
}

View File

@@ -92,42 +92,42 @@ fn test_quiet_output() -> anyhow::Result<()> {
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Expected an expression
error[invalid-syntax]
--> <temp_dir>/test.py:1:2
|
1 | ~
| ^
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
#[test]
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
let case = CliTest::with_files([(".test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Expected an expression
error[invalid-syntax]
--> .test.py:1:2
|
1 | ~
| ^
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -146,57 +146,57 @@ fn test_respect_ignore_files() -> anyhow::Result<()> {
"###);
// Test that we can set to false via CLI
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Expected an expression
error[invalid-syntax]
--> test.py:1:2
|
1 | ~
| ^
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
"###);
// Test that we can set to false via config file
case.write_file("ty.toml", "src.respect-ignore-files = false")?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Expected an expression
error[invalid-syntax]
--> test.py:1:2
|
1 | ~
| ^
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
"###);
// Ensure CLI takes precedence
case.write_file("ty.toml", "src.respect-ignore-files = true")?;
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Expected an expression
error[invalid-syntax]
--> test.py:1:2
|
1 | ~
| ^
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -782,8 +782,6 @@ impl CliTest {
let mut settings = insta::Settings::clone_current();
settings.add_filter(&tempdir_filter(&project_dir), "<temp_dir>/");
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
// 0.003s
settings.add_filter(r"\d.\d\d\ds", "0.000s");
settings.add_filter(
r#"The system cannot find the file specified."#,
"No such file or directory",

View File

@@ -26,7 +26,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -37,19 +37,12 @@ fn config_override_python_version() -> anyhow::Result<()> {
5 | print(sys.last_exc)
| ^^^^^^^^^^^^
|
info: Python 3.11 was assumed when accessing `last_exc`
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
3 | python-version = "3.11"
| ^^^^^^ Python 3.11 assumed due to this configuration setting
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
"###);
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r###"
success: true
@@ -428,94 +421,6 @@ fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn many_search_paths() -> anyhow::Result<()> {
let case = CliTest::with_files([
("extra1/foo1.py", ""),
("extra2/foo2.py", ""),
("extra3/foo3.py", ""),
("extra4/foo4.py", ""),
("extra5/foo5.py", ""),
("extra6/foo6.py", ""),
("test.py", "import foo1, baz"),
])?;
assert_cmd_snapshot!(
case.command()
.arg("--python-platform").arg("linux")
.arg("--extra-search-path").arg("extra1")
.arg("--extra-search-path").arg("extra2")
.arg("--extra-search-path").arg("extra3")
.arg("--extra-search-path").arg("extra4")
.arg("--extra-search-path").arg("extra5")
.arg("--extra-search-path").arg("extra6"),
@r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> test.py:1:14
|
1 | import foo1, baz
| ^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/extra1 (extra search path specified on the CLI or in your config file)
info: 2. <temp_dir>/extra2 (extra search path specified on the CLI or in your config file)
info: 3. <temp_dir>/extra3 (extra search path specified on the CLI or in your config file)
info: 4. <temp_dir>/extra4 (extra search path specified on the CLI or in your config file)
info: 5. <temp_dir>/extra5 (extra search path specified on the CLI or in your config file)
info: ... and 3 more paths. Run with `-v` to see all paths.
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
");
// Shows all with `-v`
assert_cmd_snapshot!(
case.command()
.arg("--python-platform").arg("linux")
.arg("--extra-search-path").arg("extra1")
.arg("--extra-search-path").arg("extra2")
.arg("--extra-search-path").arg("extra3")
.arg("--extra-search-path").arg("extra4")
.arg("--extra-search-path").arg("extra5")
.arg("--extra-search-path").arg("extra6")
.arg("-v"),
@r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> test.py:1:14
|
1 | import foo1, baz
| ^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/extra1 (extra search path specified on the CLI or in your config file)
info: 2. <temp_dir>/extra2 (extra search path specified on the CLI or in your config file)
info: 3. <temp_dir>/extra3 (extra search path specified on the CLI or in your config file)
info: 4. <temp_dir>/extra4 (extra search path specified on the CLI or in your config file)
info: 5. <temp_dir>/extra5 (extra search path specified on the CLI or in your config file)
info: 6. <temp_dir>/extra6 (extra search path specified on the CLI or in your config file)
info: 7. <temp_dir>/ (first-party code)
info: 8. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
INFO Python version: Python 3.14, platform: linux
INFO Indexed 7 file(s) in 0.000s
");
Ok(())
}
#[test]
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
let case = CliTest::with_files([
@@ -655,15 +560,15 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
error[invalid-syntax]
--> test.py:2:1
|
2 | match object():
| ^^^^^
| ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
3 | case int():
4 | pass
|
@@ -678,17 +583,17 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
Found 1 diagnostic
----- stderr -----
"#);
"###);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
error[invalid-syntax]
--> test.py:2:1
|
2 | match object():
| ^^^^^
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
3 | case int():
4 | pass
|
@@ -697,7 +602,7 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -958,7 +863,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -970,20 +875,12 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
| ^^^^^^^^^^
|
info: Python 3.10 was assumed when accessing `grantpt`
--> ty.toml:3:18
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
"###);
// Use default (which should be latest supported)
let case = CliTest::with_files([

View File

@@ -131,7 +131,7 @@ impl<'db> Completion<'db> {
| Type::BytesLiteral(_) => CompletionKind::Value,
Type::EnumLiteral(_) => CompletionKind::Enum,
Type::ProtocolInstance(_) => CompletionKind::Interface,
Type::TypeVar(_) => CompletionKind::TypeParameter,
Type::NonInferableTypeVar(_) | Type::TypeVar(_) => CompletionKind::TypeParameter,
Type::Union(union) => union
.elements(db)
.iter()

View File

@@ -336,7 +336,9 @@ impl<'db> SemanticTokenVisitor<'db> {
match ty {
Type::ClassLiteral(_) => (SemanticTokenType::Class, modifiers),
Type::TypeVar(_) => (SemanticTokenType::TypeParameter, modifiers),
Type::NonInferableTypeVar(_) | Type::TypeVar(_) => {
(SemanticTokenType::TypeParameter, modifiers)
}
Type::FunctionLiteral(_) => {
// Check if this is a method based on current scope
if self.in_class_scope {

View File

@@ -457,10 +457,6 @@ impl SemanticDb for ProjectDatabase {
fn lint_registry(&self) -> &LintRegistry {
ty_python_semantic::default_lint_registry()
}
fn verbose(&self) -> bool {
self.project().verbose(self)
}
}
#[salsa::db]
@@ -613,10 +609,6 @@ pub(crate) mod tests {
fn lint_registry(&self) -> &LintRegistry {
ty_python_semantic::default_lint_registry()
}
fn verbose(&self) -> bool {
false
}
}
#[salsa::db]

View File

@@ -113,9 +113,6 @@ pub struct Project {
/// the project including the virtual files that might exists in the editor.
#[default]
check_mode: CheckMode,
#[default]
verbose_flag: bool,
}
/// A progress reporter.
@@ -371,16 +368,6 @@ impl Project {
self.reload_files(db);
}
pub fn set_verbose(self, db: &mut dyn Db, verbose: bool) {
if self.verbose_flag(db) != verbose {
self.set_verbose_flag(db).to(verbose);
}
}
pub fn verbose(self, db: &dyn Db) -> bool {
self.verbose_flag(db)
}
/// Returns the paths that should be checked.
///
/// The default is to check the entire project in which case this method returns

View File

@@ -114,7 +114,7 @@ h: list[list[int]] = [[], [42]]
reveal_type(h) # revealed: list[list[int]]
i: list[typing.Any] = [1, 2, "3", ([4],)]
reveal_type(i) # revealed: list[Any]
reveal_type(i) # revealed: list[Any | int | str | tuple[list[Unknown | int]]]
j: list[tuple[str | int, ...]] = [(1, 2), ("foo", "bar"), ()]
reveal_type(j) # revealed: list[tuple[str | int, ...]]
@@ -123,7 +123,7 @@ k: list[tuple[list[int], ...]] = [([],), ([1, 2], [3, 4]), ([5], [6], [7])]
reveal_type(k) # revealed: list[tuple[list[int], ...]]
l: tuple[list[int], *tuple[list[typing.Any], ...], list[str]] = ([1, 2, 3], [4, 5, 6], [7, 8, 9], ["10", "11", "12"])
reveal_type(l) # revealed: tuple[list[int], list[Any], list[Any], list[str]]
reveal_type(l) # revealed: tuple[list[int], list[Any | int], list[Any | int], list[str]]
type IntList = list[int]
@@ -144,12 +144,6 @@ reveal_type(q) # revealed: dict[int | str, int]
r: dict[int | str, int | str] = {1: 1, 2: 2, 3: 3}
reveal_type(r) # revealed: dict[int | str, int | str]
s: dict[int | str, int | str]
s = {1: 1, 2: 2, 3: 3}
reveal_type(s) # revealed: dict[int | str, int | str]
(s := {1: 1, 2: 2, 3: 3})
reveal_type(s) # revealed: dict[int | str, int | str]
```
## Optional collection literal annotations are understood
@@ -187,7 +181,7 @@ h: list[list[int]] | None = [[], [42]]
reveal_type(h) # revealed: list[list[int]]
i: list[typing.Any] | None = [1, 2, "3", ([4],)]
reveal_type(i) # revealed: list[Any]
reveal_type(i) # revealed: list[Any | int | str | tuple[list[Unknown | int]]]
j: list[tuple[str | int, ...]] | None = [(1, 2), ("foo", "bar"), ()]
reveal_type(j) # revealed: list[tuple[str | int, ...]]
@@ -196,7 +190,8 @@ k: list[tuple[list[int], ...]] | None = [([],), ([1, 2], [3, 4]), ([5], [6], [7]
reveal_type(k) # revealed: list[tuple[list[int], ...]]
l: tuple[list[int], *tuple[list[typing.Any], ...], list[str]] | None = ([1, 2, 3], [4, 5, 6], [7, 8, 9], ["10", "11", "12"])
reveal_type(l) # revealed: tuple[list[int], list[Any], list[Any], list[str]]
# TODO: this should be `tuple[list[int], list[Any | int], list[Any | int], list[str]]`
reveal_type(l) # revealed: tuple[list[Unknown | int], list[Unknown | int], list[Unknown | int], list[Unknown | str]]
type IntList = list[int]
@@ -282,7 +277,7 @@ reveal_type(k) # revealed: list[Literal[1, 2, 3]]
type Y[T] = list[T]
l: Y[Y[Literal[1]]] = [[1]]
reveal_type(l) # revealed: list[Y[Literal[1]]]
reveal_type(l) # revealed: list[list[Literal[1]]]
m: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
reveal_type(m) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]
@@ -302,12 +297,6 @@ reveal_type(q) # revealed: list[int]
r: list[Literal[1, 2, 3, 4]] = [1, 2]
reveal_type(r) # revealed: list[Literal[1, 2, 3, 4]]
s: list[Literal[1]]
s = [1]
reveal_type(s) # revealed: list[Literal[1]]
(s := [1])
reveal_type(s) # revealed: list[Literal[1]]
```
## PEP-604 annotations are supported
@@ -427,14 +416,13 @@ a = f("a")
reveal_type(a) # revealed: list[Literal["a"]]
b: list[int | Literal["a"]] = f("a")
reveal_type(b) # revealed: list[Literal["a"] | int]
reveal_type(b) # revealed: list[int | Literal["a"]]
c: list[int | str] = f("a")
reveal_type(c) # revealed: list[str | int]
reveal_type(c) # revealed: list[int | str]
d: list[int | tuple[int, int]] = f((1, 2))
# TODO: We could avoid reordering the union elements here.
reveal_type(d) # revealed: list[tuple[int, int] | int]
reveal_type(d) # revealed: list[int | tuple[int, int]]
e: list[int] = f(True)
reveal_type(e) # revealed: list[int]
@@ -449,49 +437,8 @@ def f2[T: int](x: T) -> T:
return x
i: int = f2(True)
reveal_type(i) # revealed: Literal[True]
reveal_type(i) # revealed: int
j: int | str = f2(True)
reveal_type(j) # revealed: Literal[True]
```
Types are not widened unnecessarily:
```py
def id[T](x: T) -> T:
return x
def lst[T](x: T) -> list[T]:
return [x]
def _(i: int):
a: int | None = i
b: int | None = id(i)
c: int | str | None = id(i)
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
a: list[int | None] | None = [i]
b: list[int | None] | None = id([i])
c: list[int | None] | int | None = id([i])
reveal_type(a) # revealed: list[int | None]
# TODO: these should reveal `list[int | None]`
# we currently do not use the call expression annotation as type context for argument inference
reveal_type(b) # revealed: list[Unknown | int]
reveal_type(c) # revealed: list[Unknown | int]
a: list[int | None] | None = [i]
b: list[int | None] | None = lst(i)
c: list[int | None] | int | None = lst(i)
reveal_type(a) # revealed: list[int | None]
reveal_type(b) # revealed: list[int | None]
reveal_type(c) # revealed: list[int | None]
a: list | None = []
b: list | None = id([])
c: list | int | None = id([])
reveal_type(a) # revealed: list[Unknown]
reveal_type(b) # revealed: list[Unknown]
reveal_type(c) # revealed: list[Unknown]
```

View File

@@ -2363,55 +2363,6 @@ reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]
```
And cycles between many attributes:
```py
class ManyCycles:
def __init__(self: "ManyCycles"):
self.x1 = 0
self.x2 = 0
self.x3 = 0
self.x4 = 0
self.x5 = 0
self.x6 = 0
self.x7 = 1
def f1(self: "ManyCycles"):
self.x1 = self.x2 + self.x3 + self.x4 + self.x5 + self.x6 + self.x7
self.x2 = self.x1 + self.x3 + self.x4 + self.x5 + self.x6 + self.x7
self.x3 = self.x1 + self.x2 + self.x4 + self.x5 + self.x6 + self.x7
self.x4 = self.x1 + self.x2 + self.x3 + self.x5 + self.x6 + self.x7
self.x5 = self.x1 + self.x2 + self.x3 + self.x4 + self.x6 + self.x7
self.x6 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x7
self.x7 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x6
def f2(self: "ManyCycles"):
self.x1 = self.x2 + self.x3 + self.x4 + self.x5 + self.x6 + self.x7
self.x2 = self.x1 + self.x3 + self.x4 + self.x5 + self.x6 + self.x7
self.x3 = self.x1 + self.x2 + self.x4 + self.x5 + self.x6 + self.x7
self.x4 = self.x1 + self.x2 + self.x3 + self.x5 + self.x6 + self.x7
self.x5 = self.x1 + self.x2 + self.x3 + self.x4 + self.x6 + self.x7
self.x6 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x7
self.x7 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x6
def f3(self: "ManyCycles"):
self.x1 = self.x2 + self.x3 + self.x4 + self.x5 + self.x6 + self.x7
self.x2 = self.x1 + self.x3 + self.x4 + self.x5 + self.x6 + self.x7
self.x3 = self.x1 + self.x2 + self.x4 + self.x5 + self.x6 + self.x7
self.x4 = self.x1 + self.x2 + self.x3 + self.x5 + self.x6 + self.x7
self.x5 = self.x1 + self.x2 + self.x3 + self.x4 + self.x6 + self.x7
self.x6 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x7
self.x7 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x6
reveal_type(self.x1) # revealed: Unknown | int
reveal_type(self.x2) # revealed: Unknown | int
reveal_type(self.x3) # revealed: Unknown | int
reveal_type(self.x4) # revealed: Unknown | int
reveal_type(self.x5) # revealed: Unknown | int
reveal_type(self.x6) # revealed: Unknown | int
reveal_type(self.x7) # revealed: Unknown | int
```
This case additionally tests our union/intersection simplification logic:
```py
@@ -2558,28 +2509,6 @@ class C:
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
```
## Attributes of standard library modules that aren't yet defined
For attributes of stdlib modules that exist in future versions, we can give better diagnostics.
<!-- snapshot-diagnostics -->
```toml
[environment]
python-version = "3.10"
```
`main.py`:
```py
import datetime
# error: [unresolved-attribute]
reveal_type(datetime.UTC) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(datetime.fakenotreal) # revealed: Unknown
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -694,51 +694,6 @@ def _(
f1(*args10) # error: [invalid-argument-type]
```
A union of heterogeneous tuples provided to a variadic parameter:
```py
# Test inspired by ecosystem code at:
# - <https://github.com/home-assistant/core/blob/bde4eb50111a72f9717fe73ee5929e50eb06911b/homeassistant/components/lovelace/websocket.py#L50-L59>
# - <https://github.com/pydata/xarray/blob/3572f4e70f2b12ef9935c1f8c3c1b74045d2a092/xarray/tests/test_groupby.py#L3058-L3059>
def f2(a: str, b: bool): ...
def f3(coinflip: bool):
if coinflip:
args = "foo", True
else:
args = "bar", False
# revealed: tuple[Literal["foo"], Literal[True]] | tuple[Literal["bar"], Literal[False]]
reveal_type(args)
f2(*args) # fine
if coinflip:
other_args = "foo", True
else:
other_args = "bar", (True,)
# revealed: tuple[Literal["foo"], Literal[True]] | tuple[Literal["bar"], tuple[Literal[True]]]
reveal_type(other_args)
# error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `bool`, found `Literal[True] | tuple[Literal[True]]`"
f2(*other_args)
def f4(a=None, b=None, c=None, d=None, e=None): ...
my_args = ((1, 2), (3, 4), (5, 6))
for tup in my_args:
f4(*tup, e=None) # fine
my_other_args = (
(1, 2, 3, 4, 5),
(6, 7, 8, 9, 10),
)
for tup in my_other_args:
# error: [parameter-already-assigned] "Multiple values provided for parameter `e` of function `f4`"
f4(*tup, e=None)
```
### Mixed argument and parameter containing variadic
```toml

View File

@@ -122,6 +122,9 @@ class CustomerModel(ModelBase):
id: int
name: str
# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")
```
@@ -213,7 +216,11 @@ class OrderedModelBase: ...
class TestWithBase(OrderedModelBase):
inner: int
reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: bool
# TODO: No errors here, should reveal `bool`
# error: [too-many-positional-arguments]
# error: [too-many-positional-arguments]
# error: [unsupported-operator]
reveal_type(TestWithBase(1) < TestWithBase(2)) # revealed: Unknown
```
### `kw_only_default`
@@ -270,7 +277,8 @@ class ModelBase: ...
class TestBase(ModelBase):
name: str
reveal_type(TestBase.__init__) # revealed: (self: TestBase, *, name: str) -> None
# TODO: This should be `(self: TestBase, *, name: str) -> None`
reveal_type(TestBase.__init__) # revealed: def __init__(self) -> None
```
### `frozen_default`
@@ -325,9 +333,12 @@ class ModelBase: ...
class TestMeta(ModelBase):
name: str
# TODO: no error here
# error: [unknown-argument]
t = TestMeta(name="test")
t.name = "new" # error: [invalid-assignment]
# TODO: this should be an `invalid-assignment` error
t.name = "new"
```
### Combining parameters
@@ -426,15 +437,19 @@ class DefaultFrozenModel:
class Frozen(DefaultFrozenModel):
name: str
# TODO: no error here
# error: [unknown-argument]
f = Frozen(name="test")
f.name = "new" # error: [invalid-assignment]
# TODO: this should be an `invalid-assignment` error
f.name = "new"
class Mutable(DefaultFrozenModel, frozen=False):
name: str
# TODO: no error here
# error: [unknown-argument]
m = Mutable(name="test")
# TODO: This should not be an error
m.name = "new" # error: [invalid-assignment]
m.name = "new" # No error
```
## `field_specifiers`
@@ -463,7 +478,7 @@ class Person:
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
reveal_type(Person.__init__) # revealed: (self: Person, name: str = Unknown, *, age: int | None = Unknown) -> None
alice = Person("Alice", age=30)
@@ -491,7 +506,7 @@ class Person(FancyBase):
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
reveal_type(Person.__init__) # revealed: (self: Person, name: str = Unknown, *, age: int | None = Unknown) -> None
alice = Person("Alice", age=30)
@@ -500,113 +515,6 @@ reveal_type(alice.name) # revealed: str
reveal_type(alice.age) # revealed: int | None
```
### For base-class-based transformers
```py
from typing_extensions import dataclass_transform, Any
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
class FancyBase:
def __init_subclass__(cls):
...
super().__init_subclass__()
class Person(FancyBase):
id: int = fancy_field(init=False)
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
alice = Person("Alice", age=30)
reveal_type(alice.id) # revealed: int
reveal_type(alice.name) # revealed: str
reveal_type(alice.age) # revealed: int | None
```
### With default arguments
Field specifiers can have default arguments that should be respected:
```py
from typing_extensions import dataclass_transform, Any
def fancy_field(*, init: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
...
return cls
@fancy_model
class Person:
id: int = fancy_field()
name: str = fancy_field(init=True)
reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None
Person(name="Alice")
```
### With overloaded field specifiers
```py
from typing_extensions import dataclass_transform, overload, Any
@overload
def fancy_field(*, init: bool = True) -> Any: ...
@overload
def fancy_field(*, kw_only: bool = False) -> Any: ...
def fancy_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(fancy_field,))
def fancy_model[T](cls: type[T]) -> type[T]:
...
return cls
@fancy_model
class Person:
id: int = fancy_field(init=False)
name: str = fancy_field()
age: int | None = fancy_field(kw_only=True)
reveal_type(Person.__init__) # revealed: (self: Person, name: str, *, age: int | None) -> None
```
### Nested dataclass-transformers
Make sure that models are only affected by the field specifiers of their own transformer:
```py
from typing_extensions import dataclass_transform, Any
from dataclasses import field
def outer_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(outer_field,))
def outer_model[T](cls: type[T]) -> type[T]:
# ...
return cls
def inner_field(*, init: bool = True, kw_only: bool = False) -> Any: ...
@dataclass_transform(field_specifiers=(inner_field,))
def inner_model[T](cls: type[T]) -> type[T]:
# ...
return cls
@outer_model
class Outer:
@inner_model
class Inner:
inner_a: int = inner_field(init=False)
inner_b: str = outer_field(init=False)
outer_a: int = outer_field(init=False)
outer_b: str = inner_field(init=False)
reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = Any) -> None
reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = Any) -> None
```
## Overloaded dataclass-like decorators
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the

View File

@@ -497,8 +497,6 @@ class A:
a: str = field(kw_only=False)
b: int = 0
reveal_type(A.__init__) # revealed: (self: A, a: str, *, b: int = Literal[0]) -> None
A("hi")
```

View File

@@ -37,7 +37,7 @@ class Data:
content: list[int] = field(default_factory=list)
timestamp: datetime = field(default_factory=datetime.now, init=False)
# revealed: (self: Data, content: list[int] = Unknown) -> None
# revealed: (self: Data, content: list[int] = list[Unknown]) -> None
reveal_type(Data.__init__)
data = Data([1, 2, 3])
@@ -63,7 +63,6 @@ class Person:
age: int | None = field(default=None, kw_only=True)
role: str = field(default="user", kw_only=True)
# TODO: this would ideally show a default value of `None` for `age`
# revealed: (self: Person, name: str, *, age: int | None = None, role: str = Literal["user"]) -> None
reveal_type(Person.__init__)
@@ -83,8 +82,7 @@ def get_default() -> str:
reveal_type(field(default=1)) # revealed: dataclasses.Field[Literal[1]]
reveal_type(field(default=None)) # revealed: dataclasses.Field[None]
# TODO: this could ideally be `dataclasses.Field[str]` with a better generics solver
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[Unknown]
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[str]
```
## dataclass_transform field_specifiers
@@ -110,7 +108,7 @@ class A:
name: str = field(init=False)
# field(init=False) should be ignored for dataclass_transform without explicit field_specifiers
reveal_type(A.__init__) # revealed: (self: A, name: str) -> None
reveal_type(A.__init__) # revealed: (self: A, name: str = Unknown) -> None
@dataclass
class B:

View File

@@ -138,27 +138,3 @@ def _(n: int):
# error: [unknown-argument]
y = f("foo", name="bar", unknown="quux")
```
### Truncation for long unions and literals
This test demonstrates a call where the expected type is a large mixed union. The diagnostic must
therefore truncate the long expected union type to avoid overwhelming output.
```py
from typing import Literal, Union
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
class F: ...
def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int:
return 0
def _(n: int):
x = n
# error: [invalid-argument-type]
f1(x)
```

View File

@@ -192,26 +192,6 @@ from string.templatelib import Template # error: [unresolved-import]
from importlib.resources import abc # error: [unresolved-import]
```
## Attempting to import a stdlib submodule when both parts haven't yet been added
`compression` and `compression.zstd` were both added in 3.14 so there is a typeshed `VERSIONS` entry
for `compression` but not `compression.zstd`. We can't be confident `compression.zstd` exists but we
do know `compression` does and can still give good diagnostics about it.
<!-- snapshot-diagnostics -->
```toml
[environment]
python-version = "3.10"
```
```py
import compression.zstd # error: [unresolved-import]
from compression import zstd # error: [unresolved-import]
import compression.fakebutwhocansay # error: [unresolved-import]
from compression import fakebutwhocansay # error: [unresolved-import]
```
## Attempting to import a stdlib module that was previously removed
<!-- snapshot-diagnostics -->

View File

@@ -1,221 +0,0 @@
# Legacy namespace packages
## `__import__("pkgutil").extend_path`
```toml
[environment]
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
```
`/airflow-core/src/airflow/__init__.py`:
```py
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
__version__ = "3.2.0"
```
`/providers/amazon/src/airflow/__init__.py`:
```py
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
```
`/providers/amazon/src/airflow/providers/__init__.py`:
```py
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
```
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
```py
__version__ = "9.15.0"
```
`test.py`:
```py
from airflow import __version__ as airflow_version
from airflow.providers.amazon import __version__ as amazon_provider_version
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
reveal_type(amazon_provider_version) # revealed: Literal["9.15.0"]
```
## `pkgutil.extend_path`
```toml
[environment]
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
```
`/airflow-core/src/airflow/__init__.py`:
```py
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
__version__ = "3.2.0"
```
`/providers/amazon/src/airflow/__init__.py`:
```py
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
```
`/providers/amazon/src/airflow/providers/__init__.py`:
```py
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
```
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
```py
__version__ = "9.15.0"
```
`test.py`:
```py
from airflow import __version__ as airflow_version
from airflow.providers.amazon import __version__ as amazon_provider_version
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
reveal_type(amazon_provider_version) # revealed: Literal["9.15.0"]
```
## `extend_path` with keyword arguments
```toml
[environment]
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
```
`/airflow-core/src/airflow/__init__.py`:
```py
import pkgutil
__path__ = pkgutil.extend_path(name=__name__, path=__path__)
__version__ = "3.2.0"
```
`/providers/amazon/src/airflow/__init__.py`:
```py
import pkgutil
__path__ = pkgutil.extend_path(name=__name__, path=__path__)
```
`/providers/amazon/src/airflow/providers/__init__.py`:
```py
import pkgutil
__path__ = pkgutil.extend_path(name=__name__, path=__path__)
```
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
```py
__version__ = "9.15.0"
```
`test.py`:
```py
from airflow import __version__ as airflow_version
from airflow.providers.amazon import __version__ as amazon_provider_version
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
reveal_type(amazon_provider_version) # revealed: Literal["9.15.0"]
```
## incorrect `__import__` arguments
```toml
[environment]
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
```
`/airflow-core/src/airflow/__init__.py`:
```py
__path__ = __import__("not_pkgutil").extend_path(__path__, __name__)
__version__ = "3.2.0"
```
`/providers/amazon/src/airflow/__init__.py`:
```py
__path__ = __import__("not_pkgutil").extend_path(__path__, __name__)
```
`/providers/amazon/src/airflow/providers/__init__.py`:
```py
__path__ = __import__("not_pkgutil").extend_path(__path__, __name__)
```
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
```py
__version__ = "9.15.0"
```
`test.py`:
```py
from airflow.providers.amazon import __version__ as amazon_provider_version # error: [unresolved-import]
from airflow import __version__ as airflow_version
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
```
## incorrect `extend_path` arguments
```toml
[environment]
extra-paths = ["/airflow-core/src", "/providers/amazon/src/"]
```
`/airflow-core/src/airflow/__init__.py`:
```py
__path__ = __import__("pkgutil").extend_path(__path__, "other_module")
__version__ = "3.2.0"
```
`/providers/amazon/src/airflow/__init__.py`:
```py
__path__ = __import__("pkgutil").extend_path(__path__, "other_module")
```
`/providers/amazon/src/airflow/providers/__init__.py`:
```py
__path__ = __import__("pkgutil").extend_path(__path__, "other_module")
```
`/providers/amazon/src/airflow/providers/amazon/__init__.py`:
```py
__version__ = "9.15.0"
```
`test.py`:
```py
from airflow.providers.amazon import __version__ as amazon_provider_version # error: [unresolved-import]
from airflow import __version__ as airflow_version
reveal_type(airflow_version) # revealed: Literal["3.2.0"]
```

View File

@@ -260,22 +260,8 @@ def g(
reveal_type(x) # revealed: int | str
for y in b:
reveal_type(y) # revealed: str | int
```
## Union type as iterable where some elements in the union have precise tuple specs
If all elements in a union can be iterated over, we "union together" their "tuple specs" and are
able to infer the iterable element precisely when iterating over the union, in the same way that we
infer a precise type for the iterable element when iterating over a `Literal` string or bytes type:
```py
from typing import Literal
def f(x: Literal["foo", b"bar"], y: Literal["foo"] | range):
for item in x:
reveal_type(item) # revealed: Literal["f", "o", 98, 97, 114]
for item in y:
reveal_type(item) # revealed: Literal["f", "o"] | int
for z in c:
reveal_type(z) # revealed: LiteralString | int
```
## Union type as iterable where one union element has no `__iter__` method

View File

@@ -310,13 +310,13 @@ no longer valid in the inner lazy scope.
def f(l: list[str | None]):
if l[0] is not None:
def _():
reveal_type(l[0]) # revealed: str | None
reveal_type(l[0]) # revealed: str | None | Unknown
l = [None]
def f(l: list[str | None]):
l[0] = "a"
def _():
reveal_type(l[0]) # revealed: str | None
reveal_type(l[0]) # revealed: str | None | Unknown
l = [None]
def f(l: list[str | None]):

View File

@@ -71,140 +71,98 @@ reveal_type(x) # revealed: object
## Value patterns
Value patterns are evaluated by equality, which is overridable. Therefore successfully matching on
one can only give us information where we know how the subject type implements equality.
Consider the following example.
```py
from typing import Literal
def get_object() -> object:
return object()
def _(x: Literal["foo"] | int):
match x:
case "foo":
reveal_type(x) # revealed: Literal["foo"] | int
x = get_object()
match x:
case "bar":
reveal_type(x) # revealed: int
```
reveal_type(x) # revealed: object
In the first `match`'s `case "foo"` all we know is `x == "foo"`. `x` could be an instance of an
arbitrary `int` subclass with an arbitrary `__eq__`, so we can't actually narrow to
`Literal["foo"]`.
match x:
case "foo":
reveal_type(x) # revealed: Literal["foo"]
case 42:
reveal_type(x) # revealed: Literal[42]
case 6.0:
reveal_type(x) # revealed: float
case 1j:
reveal_type(x) # revealed: complex
case b"foo":
reveal_type(x) # revealed: Literal[b"foo"]
In the second `match`'s `case "bar"` we know `x == "bar"`. As discussed above, this isn't enough to
rule out `int`, but we know that `"foo" == "bar"` is false so we can eliminate `Literal["foo"]`.
More examples follow.
```py
from typing import Literal
class C:
pass
def _(x: Literal["foo", "bar", 42, b"foo"] | bool | complex):
match x:
case "foo":
reveal_type(x) # revealed: Literal["foo"] | int | float | complex
case 42:
reveal_type(x) # revealed: int | float | complex
case 6.0:
reveal_type(x) # revealed: Literal["bar", b"foo"] | (int & ~Literal[42]) | float | complex
case 1j:
reveal_type(x) # revealed: Literal["bar", b"foo"] | (int & ~Literal[42]) | float | complex
case b"foo":
reveal_type(x) # revealed: (int & ~Literal[42]) | Literal[b"foo"] | float | complex
case _:
reveal_type(x) # revealed: Literal["bar"] | (int & ~Literal[42]) | float | complex
reveal_type(x) # revealed: object
```
## Value patterns with guard
```py
from typing import Literal
def get_object() -> object:
return object()
class C:
pass
x = get_object()
def _(x: Literal["foo", b"bar"] | int):
match x:
case "foo" if reveal_type(x): # revealed: Literal["foo"] | int
pass
case b"bar" if reveal_type(x): # revealed: Literal[b"bar"] | int
pass
case 42 if reveal_type(x): # revealed: int
pass
reveal_type(x) # revealed: object
match x:
case "foo" if reveal_type(x): # revealed: Literal["foo"]
pass
case 42 if reveal_type(x): # revealed: Literal[42]
pass
case 6.0 if reveal_type(x): # revealed: float
pass
case 1j if reveal_type(x): # revealed: complex
pass
case b"foo" if reveal_type(x): # revealed: Literal[b"foo"]
pass
reveal_type(x) # revealed: object
```
## Or patterns
```py
from typing import Literal
from enum import Enum
def get_object() -> object:
return object()
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
x = get_object()
def _(color: Color):
match color:
case Color.RED | Color.GREEN:
reveal_type(color) # revealed: Literal[Color.RED, Color.GREEN]
case Color.BLUE:
reveal_type(color) # revealed: Literal[Color.BLUE]
reveal_type(x) # revealed: object
match color:
case Color.RED | Color.GREEN | Color.BLUE:
reveal_type(color) # revealed: Color
match x:
case "foo" | 42 | None:
reveal_type(x) # revealed: Literal["foo", 42] | None
case "foo" | tuple():
reveal_type(x) # revealed: tuple[Unknown, ...]
case True | False:
reveal_type(x) # revealed: bool
case 3.14 | 2.718 | 1.414:
reveal_type(x) # revealed: float
match color:
case Color.RED:
reveal_type(color) # revealed: Literal[Color.RED]
case _:
reveal_type(color) # revealed: Literal[Color.GREEN, Color.BLUE]
class A: ...
class B: ...
class C: ...
def _(x: A | B | C):
match x:
case A() | B():
reveal_type(x) # revealed: A | B
case C():
reveal_type(x) # revealed: C & ~A & ~B
case _:
reveal_type(x) # revealed: Never
match x:
case A() | B() | C():
reveal_type(x) # revealed: A | B | C
case _:
reveal_type(x) # revealed: Never
match x:
case A():
reveal_type(x) # revealed: A
case _:
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
reveal_type(x) # revealed: object
```
## Or patterns with guard
```py
from typing import Literal
def get_object() -> object:
return object()
def _(x: Literal["foo", b"bar"] | int):
match x:
case "foo" | 42 if reveal_type(x): # revealed: Literal["foo"] | int
pass
case b"bar" if reveal_type(x): # revealed: Literal[b"bar"] | int
pass
case _ if reveal_type(x): # revealed: Literal["foo", b"bar"] | int
pass
x = get_object()
reveal_type(x) # revealed: object
match x:
case "foo" | 42 | None if reveal_type(x): # revealed: Literal["foo", 42] | None
pass
case "foo" | tuple() if reveal_type(x): # revealed: Literal["foo"] | tuple[Unknown, ...]
pass
case True | False if reveal_type(x): # revealed: bool
pass
case 3.14 | 2.718 | 1.414 if reveal_type(x): # revealed: float
pass
reveal_type(x) # revealed: object
```
## Narrowing due to guard
@@ -221,7 +179,7 @@ match x:
case str() | float() if type(x) is str:
reveal_type(x) # revealed: str
case "foo" | 42 | None if isinstance(x, int):
reveal_type(x) # revealed: int
reveal_type(x) # revealed: Literal[42]
case False if x:
reveal_type(x) # revealed: Never
case "foo" if x := "bar":
@@ -243,7 +201,7 @@ reveal_type(x) # revealed: object
match x:
case str() | float() if type(x) is str and reveal_type(x): # revealed: str
pass
case "foo" | 42 | None if isinstance(x, int) and reveal_type(x): # revealed: int
case "foo" | 42 | None if isinstance(x, int) and reveal_type(x): # revealed: Literal[42]
pass
case False if x and reveal_type(x): # revealed: Never
pass

View File

@@ -360,14 +360,3 @@ type X = tuple[X, int]
def _(x: X):
reveal_type(x is x) # revealed: bool
```
### Recursive invariant
```py
type X = dict[str, X]
type Y = X | str | dict[str, Y]
def _(y: Y):
if isinstance(y, dict):
reveal_type(y) # revealed: dict[str, X] | dict[str, Y]
```

View File

@@ -1,51 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attributes.md - Attributes - Attributes of standard library modules that aren't yet defined
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
---
# Python source files
## main.py
```
1 | import datetime
2 |
3 | # error: [unresolved-attribute]
4 | reveal_type(datetime.UTC) # revealed: Unknown
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
```
# Diagnostics
```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `UTC`
--> src/main.py:4:13
|
3 | # error: [unresolved-attribute]
4 | reveal_type(datetime.UTC) # revealed: Unknown
| ^^^^^^^^^^^^
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
|
info: Python 3.10 was assumed when accessing `UTC` because it was specified on the command line
info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `fakenotreal`
--> src/main.py:6:13
|
4 | reveal_type(datetime.UTC) # revealed: Unknown
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-attribute` is enabled by default
```

View File

@@ -1,83 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: basic.md - Structures - Attempting to import a stdlib submodule when both parts haven't yet been added
mdtest path: crates/ty_python_semantic/resources/mdtest/import/basic.md
---
# Python source files
## mdtest_snippet.py
```
1 | import compression.zstd # error: [unresolved-import]
2 | from compression import zstd # error: [unresolved-import]
3 | import compression.fakebutwhocansay # error: [unresolved-import]
4 | from compression import fakebutwhocansay # error: [unresolved-import]
```
# Diagnostics
```
error[unresolved-import]: Cannot resolve imported module `compression.zstd`
--> src/mdtest_snippet.py:1:8
|
1 | import compression.zstd # error: [unresolved-import]
| ^^^^^^^^^^^^^^^^
2 | from compression import zstd # error: [unresolved-import]
3 | import compression.fakebutwhocansay # error: [unresolved-import]
|
info: The stdlib module `compression` is only available on Python 3.14+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
info: rule `unresolved-import` is enabled by default
```
```
error[unresolved-import]: Cannot resolve imported module `compression`
--> src/mdtest_snippet.py:2:6
|
1 | import compression.zstd # error: [unresolved-import]
2 | from compression import zstd # error: [unresolved-import]
| ^^^^^^^^^^^
3 | import compression.fakebutwhocansay # error: [unresolved-import]
4 | from compression import fakebutwhocansay # error: [unresolved-import]
|
info: The stdlib module `compression` is only available on Python 3.14+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
info: rule `unresolved-import` is enabled by default
```
```
error[unresolved-import]: Cannot resolve imported module `compression.fakebutwhocansay`
--> src/mdtest_snippet.py:3:8
|
1 | import compression.zstd # error: [unresolved-import]
2 | from compression import zstd # error: [unresolved-import]
3 | import compression.fakebutwhocansay # error: [unresolved-import]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 | from compression import fakebutwhocansay # error: [unresolved-import]
|
info: The stdlib module `compression` is only available on Python 3.14+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
info: rule `unresolved-import` is enabled by default
```
```
error[unresolved-import]: Cannot resolve imported module `compression`
--> src/mdtest_snippet.py:4:6
|
2 | from compression import zstd # error: [unresolved-import]
3 | import compression.fakebutwhocansay # error: [unresolved-import]
4 | from compression import fakebutwhocansay # error: [unresolved-import]
| ^^^^^^^^^^^
|
info: The stdlib module `compression` is only available on Python 3.14+
info: Python 3.10 was assumed when resolving modules because it was specified on the command line
info: rule `unresolved-import` is enabled by default
```

View File

@@ -33,13 +33,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syn
# Diagnostics
```
error[invalid-syntax]: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
error[invalid-syntax]
--> src/mdtest_snippet.py:6:19
|
4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax…
6 | return {n: [x async for x in elements(n)] for n in range(3)}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
7 | async def test():
8 | # error: [not-iterable] "Object of type `range` is not async-iterable"
|

View File

@@ -1,56 +0,0 @@
---
source: crates/ty_test/src/lib.rs
assertion_line: 427
expression: snapshot
---
---
mdtest name: union_call.md - Calling a union of function types - Try to cover all possible reasons - Truncation for long unions and literals
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import Literal, Union
2 |
3 | class A: ...
4 | class B: ...
5 | class C: ...
6 | class D: ...
7 | class E: ...
8 | class F: ...
9 |
10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int:
11 | return 0
12 |
13 | def _(n: int):
14 | x = n
15 | # error: [invalid-argument-type]
16 | f1(x)
```
# Diagnostics
```
error[invalid-argument-type]: Argument to function `f1` is incorrect
--> src/mdtest_snippet.py:16:8
|
14 | x = n
15 | # error: [invalid-argument-type]
16 | f1(x)
| ^ Expected `Literal[1, 2, 3, 4, 5, ... omitted 3 literals] | A | B | ... omitted 4 union elements`, found `int`
|
info: Function defined here
--> src/mdtest_snippet.py:10:5
|
8 | class F: ...
9 |
10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int:
| ^^ ----------------------------------------------------------- Parameter declared here
11 | return 0
|
info: rule `invalid-argument-type` is enabled by default
```

View File

@@ -20,11 +20,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/version_rela
# Diagnostics
```
error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
error[invalid-syntax]
--> src/mdtest_snippet.py:1:1
|
1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
| ^^^^^
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
2 | case 1:
3 | print("it's one")
|

View File

@@ -68,10 +68,6 @@ reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
def g(x: tuple[int, str] | tuple[bytes, bool], y: tuple[int, str] | tuple[bytes, bool, bytes]):
reveal_type(tuple(x)) # revealed: tuple[int, str] | tuple[bytes, bool]
reveal_type(tuple(y)) # revealed: tuple[int, str] | tuple[bytes, bool, bytes]
```
## Instantiating tuple subclasses

View File

@@ -233,12 +233,10 @@ Person({"name": "Alice"})
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
accepts_person({"name": "Alice"})
# TODO: this should be an error, similar to the above
house.owner = {"name": "Alice"}
a_person: Person
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
# TODO: this should be an error, similar to the above
a_person = {"name": "Alice"}
```
@@ -256,12 +254,9 @@ Person({"name": None, "age": 30})
accepts_person({"name": None, "age": 30})
# TODO: this should be an error, similar to the above
house.owner = {"name": None, "age": 30}
a_person: Person
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
# TODO: this should be an error, similar to the above
a_person = {"name": None, "age": 30}
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
(a_person := {"name": None, "age": 30})
```
All of these have an extra field that is not defined in the `TypedDict`:
@@ -278,12 +273,9 @@ Person({"name": "Alice", "age": 30, "extra": True})
accepts_person({"name": "Alice", "age": 30, "extra": True})
# TODO: this should be an error
house.owner = {"name": "Alice", "age": 30, "extra": True}
# TODO: this should be an error
a_person: Person
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
a_person = {"name": "Alice", "age": 30, "extra": True}
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
(a_person := {"name": "Alice", "age": 30, "extra": True})
```
## Type ignore compatibility issues
@@ -915,7 +907,7 @@ grandchild: Node = {"name": "grandchild", "parent": child}
nested: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": "n3", "parent": None}}}
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Node`: value of type `Literal[3]`"
# TODO: this should be an error (invalid type for `name` in innermost node)
nested_invalid: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": 3, "parent": None}}}
```

View File

@@ -1,2 +1,2 @@
spark # too many iterations (in `exported_names` query), `should not be able to access instance member `spark` of type variable IndexOpsLike@astype in inferable position`
steam.py # dependency graph cycle when querying TypeVarInstance < 'db >::lazy_default_(Id(2e007)), set cycle_fn/cycle_initial to fixpoint iterate.
spark # too many iterations (in `exported_names` query)
steam.py # hangs (single threaded)

View File

@@ -12,9 +12,6 @@ pub trait Db: SourceDb {
fn rule_selection(&self, file: File) -> &RuleSelection;
fn lint_registry(&self) -> &LintRegistry;
/// Whether ty is running with logging verbosity INFO or higher (`-v` or more).
fn verbose(&self) -> bool;
}
#[cfg(test)]
@@ -129,10 +126,6 @@ pub(crate) mod tests {
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
fn verbose(&self) -> bool {
false
}
}
#[salsa::db]

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