Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
71e07fbae3 [red-knot] mypy_primer: split installation and execution 2025-03-11 12:59:14 +01:00
593 changed files with 13920 additions and 31040 deletions

7
.github/CODEOWNERS vendored
View File

@@ -18,7 +18,6 @@
/python/py-fuzzer/ @AlexWaygood
# red-knot
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
/crates/red_knot_python_semantic @carljm @AlexWaygood @sharkdp @dcreager
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp

View File

@@ -40,17 +40,6 @@
enabled: true,
},
packageRules: [
// Pin GitHub Actions to immutable SHAs.
{
matchDepTypes: ["action"],
pinDigests: true,
},
// Annotate GitHub Actions SHAs with a SemVer version.
{
extends: ["helpers:pinGitHubActionDigests"],
extractVersion: "^(?<version>v?\\d+\\.\\d+\\.\\d+)$",
versioning: "regex:^v?(?<major>\\d+)(\\.(?<minor>\\d+)\\.(?<patch>\\d+))?$",
},
{
// Group upload/download artifact updates, the versions are dependent
groupName: "Artifact GitHub Actions dependencies",

View File

@@ -39,17 +39,17 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
@@ -59,7 +59,7 @@ jobs:
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload sdist"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist
@@ -68,23 +68,23 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --locked --out dist
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-macos-x86_64
path: dist
@@ -99,7 +99,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-macos-x86_64
path: |
@@ -110,18 +110,18 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: arm64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: aarch64
args: --release --locked --out dist
@@ -131,7 +131,7 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-aarch64-apple-darwin
path: dist
@@ -146,7 +146,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-aarch64-apple-darwin
path: |
@@ -166,18 +166,18 @@ jobs:
- target: aarch64-pc-windows-msvc
arch: x64
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -192,7 +192,7 @@ jobs:
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
@@ -203,7 +203,7 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.platform.target }}
path: |
@@ -219,18 +219,18 @@ jobs:
- x86_64-unknown-linux-gnu
- i686-unknown-linux-gnu
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -242,7 +242,7 @@ jobs:
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist
@@ -260,7 +260,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.target }}
path: |
@@ -294,24 +294,24 @@ jobs:
arch: arm
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: auto
docker-options: ${{ matrix.platform.maturin_docker_options }}
args: --release --locked --out dist
- uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2
if: ${{ matrix.platform.arch != 'ppc64' && matrix.platform.arch != 'ppc64le'}}
- uses: uraimo/run-on-arch-action@v2
if: matrix.platform.arch != 'ppc64'
name: Test wheel
with:
arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }}
@@ -325,7 +325,7 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
@@ -343,7 +343,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.platform.target }}
path: |
@@ -359,18 +359,18 @@ jobs:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -387,7 +387,7 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist
@@ -405,7 +405,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.target }}
path: |
@@ -425,23 +425,23 @@ jobs:
arch: armv7
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --locked --out dist
docker-options: ${{ matrix.platform.maturin_docker_options }}
- uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2
- uses: uraimo/run-on-arch-action@v2
name: Test wheel
with:
arch: ${{ matrix.platform.arch }}
@@ -454,7 +454,7 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/${{ env.MODULE_NAME }} --help
- name: "Upload wheels"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform.target }}
path: dist
@@ -472,7 +472,7 @@ jobs:
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.platform.target }}
path: |

View File

@@ -33,14 +33,14 @@ jobs:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@v5
with:
images: ${{ env.RUFF_BASE_IMG }}
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
@@ -79,7 +79,7 @@ jobs:
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
@@ -96,7 +96,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digests
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_TUPLE }}
path: /tmp/digests/*
@@ -113,17 +113,17 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@v5
with:
images: ${{ env.RUFF_BASE_IMG }}
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
@@ -131,7 +131,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -163,13 +163,13 @@ jobs:
# Mapping of base image followed by a comma followed by one or more base tags (comma separated)
# Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first)
image-mapping:
- alpine:3.21,alpine3.21,alpine
- alpine:3.20,alpine3.20,alpine
- debian:bookworm-slim,bookworm-slim,debian-slim
- buildpack-deps:bookworm,bookworm,debian
steps:
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -219,7 +219,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@v5
# ghcr.io prefers index level annotations
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
@@ -231,7 +231,7 @@ jobs:
${{ env.TAG_PATTERNS }}
- name: Build and push
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -256,17 +256,17 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
@@ -276,7 +276,7 @@ jobs:
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -26,152 +26,83 @@ jobs:
runs-on: ubuntu-latest
outputs:
# Flag that is raised when any code that affects parser is changed
parser: ${{ steps.check_parser.outputs.changed }}
parser: ${{ steps.changed.outputs.parser_any_changed }}
# Flag that is raised when any code that affects linter is changed
linter: ${{ steps.check_linter.outputs.changed }}
linter: ${{ steps.changed.outputs.linter_any_changed }}
# Flag that is raised when any code that affects formatter is changed
formatter: ${{ steps.check_formatter.outputs.changed }}
formatter: ${{ steps.changed.outputs.formatter_any_changed }}
# Flag that is raised when any code is changed
# This is superset of the linter and formatter
code: ${{ steps.check_code.outputs.changed }}
code: ${{ steps.changed.outputs.code_any_changed }}
# Flag that is raised when any code that affects the fuzzer is changed
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
# Flag that is set to "true" when code related to the playground changes.
playground: ${{ steps.check_playground.outputs.changed }}
fuzz: ${{ steps.changed.outputs.fuzz_any_changed }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Determine merge base
id: merge_base
env:
BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }}
run: |
sha=$(git merge-base HEAD "origin/${BASE_REF}")
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
- uses: tj-actions/changed-files@v45
id: changed
with:
files_yaml: |
parser:
- Cargo.toml
- Cargo.lock
- crates/ruff_python_trivia/**
- crates/ruff_source_file/**
- crates/ruff_text_size/**
- crates/ruff_python_ast/**
- crates/ruff_python_parser/**
- python/py-fuzzer/**
- .github/workflows/ci.yaml
- name: Check if the parser code changed
id: check_parser
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':Cargo.toml' \
':Cargo.lock' \
':crates/ruff_python_trivia/**' \
':crates/ruff_source_file/**' \
':crates/ruff_text_size/**' \
':crates/ruff_python_ast/**' \
':crates/ruff_python_parser/**' \
':python/py-fuzzer/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
linter:
- Cargo.toml
- Cargo.lock
- crates/**
- "!crates/red_knot*/**"
- "!crates/ruff_python_formatter/**"
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- scripts/*
- python/**
- .github/workflows/ci.yaml
- name: Check if the linter code changed
id: check_linter
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
':Cargo.lock' \
':crates/**' \
':!crates/red_knot*/**' \
':!crates/ruff_python_formatter/**' \
':!crates/ruff_formatter/**' \
':!crates/ruff_dev/**' \
':!crates/ruff_db/**' \
':scripts/*' \
':python/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
formatter:
- Cargo.toml
- Cargo.lock
- crates/ruff_python_formatter/**
- crates/ruff_formatter/**
- crates/ruff_python_trivia/**
- crates/ruff_python_ast/**
- crates/ruff_source_file/**
- crates/ruff_python_index/**
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
- python/**
- .github/workflows/ci.yaml
- name: Check if the formatter code changed
id: check_formatter
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
':Cargo.lock' \
':crates/ruff_python_formatter/**' \
':crates/ruff_formatter/**' \
':crates/ruff_python_trivia/**' \
':crates/ruff_python_ast/**' \
':crates/ruff_source_file/**' \
':crates/ruff_python_index/**' \
':crates/ruff_python_index/**' \
':crates/ruff_text_size/**' \
':crates/ruff_python_parser/**' \
':scripts/*' \
':python/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
fuzz:
- fuzz/Cargo.toml
- fuzz/Cargo.lock
- fuzz/fuzz_targets/**
- name: Check if the fuzzer code changed
id: check_fuzzer
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
':Cargo.lock' \
':fuzz/fuzz_targets/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Check if there was any code related change
id: check_code
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**/*' \
':!**/*.md' \
':crates/red_knot_python_semantic/resources/mdtest/**/*.md' \
':!docs/**' \
':!assets/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Check if there was any playground related change
id: check_playground
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':playground/**' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
code:
- "**/*"
- "!**/*.md"
- "crates/red_knot_python_semantic/resources/mdtest/**/*.md"
- "!docs/**"
- "!assets/**"
cargo-fmt:
name: "cargo fmt"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
@@ -185,10 +116,10 @@ jobs:
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -205,20 +136,20 @@ jobs:
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -239,7 +170,7 @@ jobs:
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
- uses: actions/upload-artifact@v4
with:
name: ruff
path: target/debug/ruff
@@ -251,20 +182,20 @@ jobs:
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -280,14 +211,14 @@ jobs:
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -307,18 +238,18 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- name: "Test ruff_wasm"
@@ -336,10 +267,10 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -354,15 +285,15 @@ jobs:
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0
- uses: SebRollen/toml-action@v1.2.0
id: msrv
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
env:
MSRV: ${{ steps.msrv.outputs.value }}
@@ -370,11 +301,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -391,10 +322,10 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
with:
workspaces: "fuzz -> target"
- name: "Install Rust toolchain"
@@ -419,11 +350,11 @@ jobs:
env:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: astral-sh/setup-uv@v5
- uses: actions/download-artifact@v4
name: Download Ruff binary to test
id: download-cached-binary
with:
@@ -453,10 +384,10 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -485,21 +416,21 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: actions/download-artifact@v4
name: Download comparison Ruff binary
id: ruff-target
with:
name: ruff
path: target/debug
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
- uses: dawidd6/action-download-artifact@v8
name: Download baseline Ruff binary
with:
name: ruff
@@ -587,13 +518,13 @@ jobs:
run: |
echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
- uses: actions/upload-artifact@v4
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
- uses: actions/upload-artifact@v4
name: Upload Results
with:
name: ecosystem-result
@@ -605,7 +536,7 @@ jobs:
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@main
@@ -618,18 +549,18 @@ jobs:
timeout-minutes: 20
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@36db84001d74475ad1b8e6613557ae4ee2dc3598 # v1
uses: PyO3/maturin-action@v1
with:
args: --out dist
- name: "Test wheel"
@@ -645,19 +576,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install pre-commit"
run: pip install pre-commit
- name: "Cache pre-commit"
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -679,22 +610,22 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
uses: astral-sh/setup-uv@v5
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -721,10 +652,10 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Run checks"
@@ -743,21 +674,21 @@ jobs:
- determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
steps:
- uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2
- uses: extractions/setup-just@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
name: "Download ruff-lsp source"
with:
persist-credentials: false
repository: "astral-sh/ruff-lsp"
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
- uses: actions/download-artifact@v4
name: Download development ruff binary
id: ruff-target
with:
@@ -780,39 +711,6 @@ jobs:
just test
check-playground:
name: "check playground"
runs-on: ubuntu-latest
timeout-minutes: 5
needs:
- determine_changes
if: ${{ (needs.determine_changes.outputs.playground == 'true') }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: 22
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"
run: npm ci
working-directory: playground
- name: "Build playgrounds"
run: npm run dev:wasm
working-directory: playground
- name: "Run TypeScript checks"
run: npm run check
working-directory: playground
- name: "Check formatting"
run: npm run fmt:check
working-directory: playground
benchmarks:
runs-on: ubuntu-24.04
needs: determine_changes
@@ -820,17 +718,17 @@ jobs:
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@2c41309d51ede152b6f2ee6bf3b71e6dc9a8b7df # v2
uses: taiki-e/install-action@v2
with:
tool: cargo-codspeed
@@ -838,7 +736,7 @@ jobs:
run: cargo codspeed build --features codspeed -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3
uses: CodSpeedHQ/action@v3
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -31,15 +31,15 @@ jobs:
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
- uses: astral-sh/setup-uv@v5
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: Build ruff
# A debug build means the script runs slower once it gets started,
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
@@ -65,7 +65,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -30,14 +30,14 @@ jobs:
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
@@ -59,7 +59,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -28,16 +28,16 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
uses: astral-sh/setup-uv@v5
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
with:
workspaces: "ruff"
- name: Install Rust toolchain
@@ -68,7 +68,7 @@ jobs:
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument)$' \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow)$' \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]
@@ -81,13 +81,13 @@ jobs:
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: mypy_primer_diff
path: mypy_primer.diff
- name: Upload pr-number
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: pr-number
path: pr-number

View File

@@ -16,7 +16,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
- uses: dawidd6/action-download-artifact@v8
name: Download PR number
with:
name: pr-number
@@ -32,7 +32,7 @@ jobs:
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
- uses: dawidd6/action-download-artifact@v8
name: "Download mypy_primer results"
id: download-mypy_primer_diff
if: steps.pr-number.outputs.pr-number
@@ -79,7 +79,7 @@ jobs:
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
uses: peter-evans/find-comment@v3
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
@@ -89,7 +89,7 @@ jobs:
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
uses: actions/github-script@v7
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |

View File

@@ -16,7 +16,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
- uses: dawidd6/action-download-artifact@v8
name: Download pull request number
with:
name: pr-number
@@ -32,7 +32,7 @@ jobs:
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
- uses: dawidd6/action-download-artifact@v8
name: "Download ecosystem results"
id: download-ecosystem-result
if: steps.pr-number.outputs.pr-number
@@ -70,7 +70,7 @@ jobs:
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
uses: peter-evans/find-comment@v3
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
@@ -80,7 +80,7 @@ jobs:
- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}

View File

@@ -23,12 +23,12 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
persist-credentials: true
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5
- uses: actions/setup-python@v5
with:
python-version: 3.12
@@ -61,14 +61,14 @@ jobs:
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@dc588b651fe13675774614f8e6a936a468676387 # v0.9.0
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
- uses: Swatinem/rust-cache@v2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}

View File

@@ -1,58 +0,0 @@
# Publish the Red Knot playground.
name: "[Knot Playground] Release"
permissions: {}
on:
push:
branches: [main]
paths:
- "crates/red_knot*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- "playground"
- ".github/workflows/publish-knot-playground.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
publish:
runs-on: ubuntu-latest
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"
run: npm ci
working-directory: playground
- name: "Run TypeScript checks"
run: npm run check
working-directory: playground
- name: "Build Knot playground"
run: npm run build --workspace knot-playground
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
command: pages deploy playground/knot/dist --project-name=knot-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}

View File

@@ -24,31 +24,36 @@ jobs:
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 20
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@v0.2.0
- name: "Run wasm-pack"
run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff_wasm
- name: "Install Node dependencies"
run: npm ci
working-directory: playground
- name: "Run TypeScript checks"
run: npm run check
working-directory: playground
- name: "Build Ruff playground"
run: npm run build --workspace ruff-playground
- name: "Build JavaScript bundle"
run: npm run build
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
command: pages deploy playground/ruff/dist --project-name=ruff-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
command: pages deploy playground/dist --project-name=ruff-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}

View File

@@ -22,8 +22,8 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5
- uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: astral-sh/setup-uv@v5
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: wheels

View File

@@ -29,15 +29,15 @@ jobs:
target: [web, bundler, nodejs]
fail-fast: false
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- uses: jetli/wasm-bindgen-action@v0.2.0
- name: "Run wasm-pack build"
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
- name: "Rename generated package"
@@ -45,7 +45,7 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -50,7 +50,7 @@ on:
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "depot-ubuntu-latest-4"
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }}
@@ -59,7 +59,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dist
@@ -68,7 +68,7 @@ jobs:
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -84,7 +84,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -116,23 +116,23 @@ jobs:
- plan
- custom-build-binaries
- custom-build-docker
runs-on: "depot-ubuntu-latest-4"
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
@@ -150,7 +150,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
@@ -167,22 +167,22 @@ jobs:
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "depot-ubuntu-latest-4"
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
@@ -196,7 +196,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -242,16 +242,16 @@ jobs:
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-wasm.result == 'skipped' || needs.custom-publish-wasm.result == 'success') }}
runs-on: "depot-ubuntu-latest-4"
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
submodules: recursive
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts

View File

@@ -21,12 +21,12 @@ jobs:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
name: Checkout Ruff
with:
path: ruff
persist-credentials: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
name: Checkout typeshed
with:
repository: python/typeshed
@@ -70,7 +70,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -1 +0,0 @@
!/.github/

View File

@@ -19,7 +19,7 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24
rev: v0.23
hooks:
- id: validate-pyproject
@@ -60,7 +60,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.30.2
rev: v1.30.0
hooks:
- id: typos
@@ -74,7 +74,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
rev: v0.9.9
hooks:
- id: ruff-format
- id: ruff
@@ -84,7 +84,7 @@ repos:
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.5.3
rev: v3.5.2
hooks:
- id: prettier
types: [yaml]
@@ -92,12 +92,12 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.5.1
rev: v1.4.1
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.3
rev: 0.31.2
hooks:
- id: check-github-workflows

View File

@@ -1,55 +1,5 @@
# Breaking Changes
## 0.11.0
This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`.
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
In previous versions of Ruff, you could specify your Python version with:
- The `target-version` option in a `ruff.toml` file or the `[tool.ruff]` section of a pyproject.toml file.
- The `project.requires-python` field in a `pyproject.toml` file with a `[tool.ruff]` section.
These options worked well in most cases, and are still recommended for fine control of the Python version. However, because of the way Ruff discovers config files, `pyproject.toml` files without a `[tool.ruff]` section would be ignored, including the `requires-python` setting. Ruff would then use the default Python version (3.9 as of this writing) instead, which is surprising when you've attempted to request another version.
In v0.10, config discovery has been updated to address this issue:
- If Ruff finds a `ruff.toml` file without a `target-version`, it will check
for a `pyproject.toml` file in the same directory and respect its
`requires-python` version, even if it does not contain a `[tool.ruff]`
section.
- If Ruff finds a user-level configuration, the `requires-python` field of the closest `pyproject.toml` in a parent directory will take precedence.
- If there is no config file (`ruff.toml`or `pyproject.toml` with a
`[tool.ruff]` section) in the directory of the file being checked, Ruff will
search for the closest `pyproject.toml` in the parent directories and use its
`requires-python` setting.
## 0.10.0
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0.
You can find a description of this change in the 0.11.0 section.
- **Updated `TYPE_CHECKING` behavior** ([#16669](https://github.com/astral-sh/ruff/pull/16669))
Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead.
- **More robust noqa parsing** ([#16483](https://github.com/astral-sh/ruff/pull/16483))
The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [_Error suppression_](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.
- **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#14005](https://github.com/astral-sh/ruff/pull/14005))
This change fixes a bug in the formatter where it introduced unnecessary parentheses around with statements with a single context manager and a trailing comment. This change may result in a change in formatting for some users.
- **Bump alpine default tag to 3.21 for derived Docker images** ([#16456](https://github.com/astral-sh/ruff/pull/16456))
Alpine 3.21 was released in Dec 2024 and is used in the official Alpine-based Python images. Now the ruff:alpine image will use 3.21 instead of 3.20 and ruff:alpine3.20 will no longer be updated.
- **\[`unsafe-markup-use`\]: `RUF035` has been recoded to `S704`** ([#15957](https://github.com/astral-sh/ruff/pull/15957))
## 0.9.0
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the [changelog](./CHANGELOG.md#090) for a detailed list of changes.

View File

@@ -1,175 +1,5 @@
# Changelog
## 0.11.1
### Preview features
- \[`airflow`\] Add `chain`, `chain_linear` and `cross_downstream` for `AIR302` ([#16647](https://github.com/astral-sh/ruff/pull/16647))
- [syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors ([#16581](https://github.com/astral-sh/ruff/pull/16581))
- [syntax-errors] PEP 701 f-strings before Python 3.12 ([#16543](https://github.com/astral-sh/ruff/pull/16543))
- [syntax-errors] Parenthesized context managers before Python 3.9 ([#16523](https://github.com/astral-sh/ruff/pull/16523))
- [syntax-errors] Star annotations before Python 3.11 ([#16545](https://github.com/astral-sh/ruff/pull/16545))
- [syntax-errors] Star expression in index before Python 3.11 ([#16544](https://github.com/astral-sh/ruff/pull/16544))
- [syntax-errors] Unparenthesized assignment expressions in sets and indexes ([#16404](https://github.com/astral-sh/ruff/pull/16404))
### Bug fixes
- Server: Allow `FixAll` action in presence of version-specific syntax errors ([#16848](https://github.com/astral-sh/ruff/pull/16848))
- \[`flake8-bandit`\] Allow raw strings in `suspicious-mark-safe-usage` (`S308`) #16702 ([#16770](https://github.com/astral-sh/ruff/pull/16770))
- \[`refurb`\] Avoid panicking `unwrap` in `verbose-decimal-constructor` (`FURB157`) ([#16777](https://github.com/astral-sh/ruff/pull/16777))
- \[`refurb`\] Fix starred expressions fix (`FURB161`) ([#16550](https://github.com/astral-sh/ruff/pull/16550))
- Fix `--statistics` reporting for unsafe fixes ([#16756](https://github.com/astral-sh/ruff/pull/16756))
### Rule changes
- \[`flake8-executables`\] Allow `uv run` in shebang line for `shebang-missing-python` (`EXE003`) ([#16849](https://github.com/astral-sh/ruff/pull/16849),[#16855](https://github.com/astral-sh/ruff/pull/16855))
### CLI
- Add `--exit-non-zero-on-format` ([#16009](https://github.com/astral-sh/ruff/pull/16009))
### Documentation
- Update Ruff tutorial to avoid non-existent fix in `__init__.py` ([#16818](https://github.com/astral-sh/ruff/pull/16818))
- \[`flake8-gettext`\] Swap `format-` and `printf-in-get-text-func-call` examples (`INT002`, `INT003`) ([#16769](https://github.com/astral-sh/ruff/pull/16769))
## 0.11.0
This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`.
### Breaking changes
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
In previous versions of Ruff, you could specify your Python version with:
- The `target-version` option in a `ruff.toml` file or the `[tool.ruff]` section of a pyproject.toml file.
- The `project.requires-python` field in a `pyproject.toml` file with a `[tool.ruff]` section.
These options worked well in most cases, and are still recommended for fine control of the Python version. However, because of the way Ruff discovers config files, `pyproject.toml` files without a `[tool.ruff]` section would be ignored, including the `requires-python` setting. Ruff would then use the default Python version (3.9 as of this writing) instead, which is surprising when you've attempted to request another version.
In v0.10, config discovery has been updated to address this issue:
- If Ruff finds a `ruff.toml` file without a `target-version`, it will check
for a `pyproject.toml` file in the same directory and respect its
`requires-python` version, even if it does not contain a `[tool.ruff]`
section.
- If Ruff finds a user-level configuration, the `requires-python` field of the closest `pyproject.toml` in a parent directory will take precedence.
- If there is no config file (`ruff.toml`or `pyproject.toml` with a
`[tool.ruff]` section) in the directory of the file being checked, Ruff will
search for the closest `pyproject.toml` in the parent directories and use its
`requires-python` setting.
### Stabilization
The following behaviors have been stabilized:
- [`blanket-noqa`](https://docs.astral.sh/ruff/rules/blanket-noqa/) (`PGH004`): Also detect blanked file-level noqa comments (and not just line level comments).
### Preview features
- [syntax-errors] Tuple unpacking in `for` statement iterator clause before Python 3.9 ([#16558](https://github.com/astral-sh/ruff/pull/16558))
## 0.10.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.10.0) for a migration guide and overview of the changes!
### Breaking changes
See also, the "Remapped rules" section which may result in disabled rules.
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0.
You can find a description of this change in the 0.11.0 section.
- **Updated `TYPE_CHECKING` behavior** ([#16669](https://github.com/astral-sh/ruff/pull/16669))
Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead.
- **More robust noqa parsing** ([#16483](https://github.com/astral-sh/ruff/pull/16483))
The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [*Error suppression*](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.
- **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#14005](https://github.com/astral-sh/ruff/pull/14005))
This change fixes a bug in the formatter where it introduced unnecessary parentheses around with statements with a single context manager and a trailing comment. This change may result in a change in formatting for some users.
- **Bump alpine default tag to 3.21 for derived Docker images** ([#16456](https://github.com/astral-sh/ruff/pull/16456))
Alpine 3.21 was released in Dec 2024 and is used in the official Alpine-based Python images. Now the ruff:alpine image will use 3.21 instead of 3.20 and ruff:alpine3.20 will no longer be updated.
### Deprecated Rules
The following rules have been deprecated:
- [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance/) (`UP038`)
- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/) (`S320`)
### Remapped rules
The following rules have been remapped to new rule codes:
- \[`unsafe-markup-use`\]: `RUF035` to `S704`
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`batched-without-explicit-strict`](https://docs.astral.sh/ruff/rules/batched-without-explicit-strict) (`B911`)
- [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable) (`C420`)
- [`datetime-min-max`](https://docs.astral.sh/ruff/rules/datetime-min-max) (`DTZ901`)
- [`fast-api-unused-path-parameter`](https://docs.astral.sh/ruff/rules/fast-api-unused-path-parameter) (`FAST003`)
- [`root-logger-call`](https://docs.astral.sh/ruff/rules/root-logger-call) (`LOG015`)
- [`len-test`](https://docs.astral.sh/ruff/rules/len-test) (`PLC1802`)
- [`shallow-copy-environ`](https://docs.astral.sh/ruff/rules/shallow-copy-environ) (`PLW1507`)
- [`os-listdir`](https://docs.astral.sh/ruff/rules/os-listdir) (`PTH208`)
- [`invalid-pathlib-with-suffix`](https://docs.astral.sh/ruff/rules/invalid-pathlib-with-suffix) (`PTH210`)
- [`invalid-assert-message-literal-argument`](https://docs.astral.sh/ruff/rules/invalid-assert-message-literal-argument) (`RUF040`)
- [`unnecessary-nested-literal`](https://docs.astral.sh/ruff/rules/unnecessary-nested-literal) (`RUF041`)
- [`unnecessary-cast-to-int`](https://docs.astral.sh/ruff/rules/unnecessary-cast-to-int) (`RUF046`)
- [`map-int-version-parsing`](https://docs.astral.sh/ruff/rules/map-int-version-parsing) (`RUF048`)
- [`if-key-in-dict-del`](https://docs.astral.sh/ruff/rules/if-key-in-dict-del) (`RUF051`)
- [`unsafe-markup-use`](https://docs.astral.sh/ruff/rules/unsafe-markup-use) (`S704`). This rule has also been renamed from `RUF035`.
- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string) (`SIM905`)
- [`runtime-cast-value`](https://docs.astral.sh/ruff/rules/runtime-cast-value) (`TC006`)
- [`unquoted-type-alias`](https://docs.astral.sh/ruff/rules/unquoted-type-alias) (`TC007`)
- [`non-pep646-unpack`](https://docs.astral.sh/ruff/rules/non-pep646-unpack) (`UP044`)
The following behaviors have been stabilized:
- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) [`invalid-first-argument-name-for-class-method`](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/) (`N804`): `__new__` methods are now no longer flagged by `invalid-first-argument-name-for-class-method` (`N804`) but instead by `bad-staticmethod-argument` (`PLW0211`)
- [`bad-str-strip-call`](https://docs.astral.sh/ruff/rules/bad-str-strip-call/) (`PLE1310`): The rule now applies to objects which are known to have type `str` or `bytes`.
- [`custom-type-var-for-self`](https://docs.astral.sh/ruff/rules/custom-type-var-for-self/) (`PYI019`): More accurate detection of custom `TypeVars` replaceable by `Self`. The range of the diagnostic is now the full function header rather than just the return annotation.
- [`invalid-argument-name`](https://docs.astral.sh/ruff/rules/invalid-argument-name/) (`N803`): Ignore argument names of functions decorated with `typing.override`
- [`invalid-envvar-default`](https://docs.astral.sh/ruff/rules/invalid-envvar-default/) (`PLW1508`): Detect default value arguments to `os.environ.get` with invalid type.
- [`pytest-raises-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-raises-with-multiple-statements/) (`PT012`) [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements/) (`PT031`): Allow `for` statements with an empty body in `pytest.raises` and `pytest.warns` `with` statements.
- [`redundant-open-modes`](https://docs.astral.sh/ruff/rules/redundant-open-modes/) (`UP015`): The diagnostic range is now the range of the redundant mode argument where it previously was the range of the entire open call. You may have to replace your `noqa` comments when suppressing `UP015`.
- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`): Changes the default value of `lint.flake8-builtins.strict-checking` from `true` to `false`.
- [`type-none-comparison`](https://docs.astral.sh/ruff/rules/type-none-comparison/) (`FURB169`): Now also recognizes `type(expr) is type(None)` comparisons where `expr` isn't a name expression.
The following fixes or improvements to fixes have been stabilized:
- [`repeated-equality-comparison`](https://docs.astral.sh/ruff/rules/repeated-equality-comparison/) (`PLR1714`) ([#16685](https://github.com/astral-sh/ruff/pull/16685))
- [`needless-bool`](https://docs.astral.sh/ruff/rules/needless-bool/) (`SIM103`) ([#16684](https://github.com/astral-sh/ruff/pull/16684))
- [`unused-private-type-var`](https://docs.astral.sh/ruff/rules/unused-private-type-var/) (`PYI018`) ([#16682](https://github.com/astral-sh/ruff/pull/16682))
### Server
- Remove logging output for `ruff.printDebugInformation` ([#16617](https://github.com/astral-sh/ruff/pull/16617))
### Configuration
- \[`flake8-builtins`\] Deprecate the `builtins-` prefixed options in favor of the unprefixed options (e.g. `builtins-allowed-modules` is now deprecated in favor of `allowed-modules`) ([#16092](https://github.com/astral-sh/ruff/pull/16092))
### Bug fixes
- [flake8-bandit] Fix mixed-case hash algorithm names (S324) ([#16552](https://github.com/astral-sh/ruff/pull/16552))
### CLI
- [ruff] Fix `last_tag`/`commits_since_last_tag` for `version` command ([#16686](https://github.com/astral-sh/ruff/pull/16686))
## 0.9.10
### Preview features

447
Cargo.lock generated
View File

@@ -46,13 +46,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "annotate-snippets"
version = "0.11.5"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4"
dependencies = [
"anstyle",
"unicode-width 0.2.0",
]
checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7"
[[package]]
name = "anstream"
@@ -272,9 +268,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.16"
version = "1.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf"
dependencies = [
"jobserver",
"libc",
@@ -293,6 +289,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chic"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c"
dependencies = [
"annotate-snippets",
]
[[package]]
name = "chrono"
version = "0.4.40"
@@ -334,9 +339,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.32"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +349,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.32"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
dependencies = [
"anstream",
"anstyle",
@@ -357,9 +362,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.46"
version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98"
checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6"
dependencies = [
"clap",
]
@@ -387,9 +392,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.32"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck",
"proc-macro2",
@@ -418,9 +423,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.9.1"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60e744216bfa9add3b1f2505587cbbb837923232ed10963609f4a6e3cbd99c3e"
checksum = "de4b67ff8985f3993f06167d71cf4aec178b0a1580f91a987170c59d60021103"
dependencies = [
"colored 2.2.0",
"libc",
@@ -431,38 +436,13 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "2.9.1"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5926ca63222a35b9a2299adcaafecf596efe20a9a2048e4a81cb2fc3463b4a8"
checksum = "68403d768ed1def18a87e2306676781314448393ecf0d3057c4527cabf524a3d"
dependencies = [
"codspeed",
"codspeed-criterion-compat-walltime",
"colored 2.2.0",
]
[[package]]
name = "codspeed-criterion-compat-walltime"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbae4da05076cbc673e242400ac8f4353bdb686e48020edc6e36a5c36ae0878e"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"codspeed",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
"criterion",
]
[[package]]
@@ -478,7 +458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -487,7 +467,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -495,20 +475,6 @@ name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "compact_str"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
dependencies = [
"castaway",
"cfg-if",
@@ -521,9 +487,9 @@ dependencies = [
[[package]]
name = "console"
version = "0.15.11"
version = "0.15.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
dependencies = [
"encode_unicode",
"libc",
@@ -860,15 +826,15 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.19"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
[[package]]
name = "either"
version = "1.15.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "encode_unicode"
@@ -894,22 +860,22 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.11.7"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
@@ -918,7 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -941,13 +907,13 @@ dependencies = [
[[package]]
name = "etcetera"
version = "0.10.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -979,9 +945,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.0"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -995,9 +961,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "form_urlencoded"
@@ -1115,9 +1081,9 @@ dependencies = [
[[package]]
name = "half"
version = "2.5.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
@@ -1163,17 +1129,17 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.5.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "home"
version = "0.5.11"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1185,6 +1151,12 @@ dependencies = [
"utf8-width",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@@ -1390,9 +1362,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -1493,13 +1465,13 @@ dependencies = [
[[package]]
name = "is-terminal"
version = "0.4.16"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
dependencies = [
"hermit-abi 0.5.0",
"hermit-abi 0.4.0",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1547,33 +1519,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
@@ -1628,17 +1576,17 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]]
name = "libcst"
version = "1.7.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad9e315e3f679e61b9095ffd5e509de78b8a4ea3bba9d772f6fb243209f808d4"
checksum = "649801a698a649791541a3125d396d5db065ed7cea53faca3652b0179394922a"
dependencies = [
"annotate-snippets",
"chic",
"libcst_derive",
"memchr",
"paste",
@@ -1649,9 +1597,9 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "1.7.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa96ed35d0dccc67cf7ba49350cb86de3dcb1d072a7ab28f99117f19d874953"
checksum = "3bf66548c351bcaed792ef3e2b430cc840fbde504e09da6b29ed114ca60dcd4b"
dependencies = [
"quote",
"syn 2.0.100",
@@ -1702,17 +1650,11 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]]
name = "litemap"
version = "0.7.5"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lock_api"
@@ -1822,9 +1764,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler2",
]
@@ -1955,15 +1897,15 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.21.1"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "oorandom"
version = "11.1.5"
version = "11.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
[[package]]
name = "option-ext"
@@ -1973,9 +1915,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.6"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e98f974336ceffd5b1b1f4fcbb89a23c8dcd334adc4b8612f11b7fa99944535"
checksum = "c55fdf45a2b1e929e3656d404395767e05c98b6ebd8157eb31e370077d545160"
dependencies = [
"indexmap",
]
@@ -1992,9 +1934,9 @@ dependencies = [
[[package]]
name = "os_str_bytes"
version = "7.1.0"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86e2db86dd008b4c88c77a9bb83d9286bf77204e255bb3fda3b2eebcae66b62"
checksum = "7ac44c994af577c799b1b4bd80dc214701e349873ad894d6cdf96f4f7526e0b9"
dependencies = [
"memchr",
]
@@ -2066,9 +2008,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "peg"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477"
checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f"
dependencies = [
"peg-macros",
"peg-runtime",
@@ -2076,9 +2018,9 @@ dependencies = [
[[package]]
name = "peg-macros"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71"
checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426"
dependencies = [
"peg-runtime",
"proc-macro2",
@@ -2087,9 +2029,9 @@ dependencies = [
[[package]]
name = "peg-runtime"
version = "0.8.5"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca"
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
[[package]]
name = "pep440_rs"
@@ -2217,18 +2159,18 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.10"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [
"proc-macro2",
"quote",
@@ -2243,32 +2185,23 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pkg-config"
version = "0.3.32"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "portable-atomic"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -2377,9 +2310,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
dependencies = [
"proc-macro2",
]
@@ -2402,8 +2335,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy",
"rand_core 0.9.0",
"zerocopy 0.8.14",
]
[[package]]
@@ -2423,7 +2356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
"rand_core 0.9.0",
]
[[package]]
@@ -2437,11 +2370,12 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.3"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.14",
]
[[package]]
@@ -2531,7 +2465,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"camino",
"compact_str 0.9.0",
"compact_str",
"countme",
"dir-test",
"drop_bomb",
@@ -2644,17 +2578,15 @@ dependencies = [
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
"ruff_source_file",
"ruff_text_size",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.9.0",
]
@@ -2727,7 +2659,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.1"
version = "0.9.10"
dependencies = [
"anyhow",
"argfile",
@@ -2962,7 +2894,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.1"
version = "0.9.10"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3057,7 +2989,7 @@ version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.0",
"compact_str 0.9.0",
"compact_str",
"is-macro",
"itertools 0.14.0",
"memchr",
@@ -3153,7 +3085,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"bstr",
"compact_str 0.9.0",
"compact_str",
"insta",
"memchr",
"ruff_annotate_snippets",
@@ -3260,7 +3192,6 @@ dependencies = [
"thiserror 2.0.12",
"toml",
"tracing",
"tracing-log",
"tracing-subscriber",
]
@@ -3285,7 +3216,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.1"
version = "0.9.10"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3378,49 +3309,35 @@ dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
dependencies = [
"bitflags 2.9.0",
"errno",
"libc",
"linux-raw-sys 0.9.3",
"windows-sys 0.59.0",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.20"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.20"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=99be5d9917c3dd88e19735a82ef6bf39ba84bd7e#99be5d9917c3dd88e19735a82ef6bf39ba84bd7e"
dependencies = [
"boxcar",
"compact_str 0.8.1",
"compact_str",
"crossbeam-queue",
"dashmap 6.1.0",
"hashbrown 0.15.2",
"hashlink",
"indexmap",
"parking_lot",
"portable-atomic",
"rayon",
"rustc-hash 2.1.1",
"salsa-macro-rules",
@@ -3431,13 +3348,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=99be5d9917c3dd88e19735a82ef6bf39ba84bd7e#99be5d9917c3dd88e19735a82ef6bf39ba84bd7e"
[[package]]
name = "salsa-macros"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=99be5d9917c3dd88e19735a82ef6bf39ba84bd7e#99be5d9917c3dd88e19735a82ef6bf39ba84bd7e"
dependencies = [
"heck",
"proc-macro2",
@@ -3553,9 +3470,9 @@ dependencies = [
[[package]]
name = "serde_repr"
version = "0.1.20"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
@@ -3769,15 +3686,16 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.19.0"
version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix 1.0.2",
"windows-sys 0.59.0",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
@@ -3791,11 +3709,11 @@ dependencies = [
[[package]]
name = "terminal_size"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9"
dependencies = [
"rustix 1.0.2",
"rustix",
"windows-sys 0.59.0",
]
@@ -3951,9 +3869,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.9.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [
"tinyvec_macros",
]
@@ -3987,9 +3905,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.24"
version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [
"indexmap",
"serde",
@@ -4115,9 +4033,9 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.18.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
@@ -4266,9 +4184,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [
"getrandom 0.3.1",
"js-sys",
@@ -4279,9 +4197,9 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.16.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862"
checksum = "d28dd23acb5f2fa7bd2155ab70b960e770596b3bb6395119b40476c3655dfba4"
dependencies = [
"proc-macro2",
"quote",
@@ -4353,9 +4271,9 @@ dependencies = [
[[package]]
name = "wait-timeout"
version = "0.2.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
@@ -4502,13 +4420,13 @@ dependencies = [
[[package]]
name = "which"
version = "7.0.2"
version = "7.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
dependencies = [
"either",
"env_home",
"rustix 0.38.44",
"rustix",
"winsafe",
]
@@ -4543,7 +4461,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4781,9 +4699,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419"
dependencies = [
"memchr",
]
@@ -4847,18 +4765,39 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.23"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
"byteorder",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
dependencies = [
"zerocopy-derive 0.8.14",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
dependencies = [
"proc-macro2",
"quote",
@@ -4867,18 +4806,18 @@ dependencies = [
[[package]]
name = "zerofrom"
version = "0.1.6"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
@@ -4942,9 +4881,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.14+zstd.1.5.7"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",

View File

@@ -63,7 +63,7 @@ colored = { version = "3.0.0" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
compact_str = "0.8.0"
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
@@ -71,7 +71,7 @@ dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
etcetera = { version = "0.10.0" }
etcetera = { version = "0.8.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d758691ba17ee1a60c5356ea90888d529e1782ad" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "99be5d9917c3dd88e19735a82ef6bf39ba84bd7e" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -154,7 +154,6 @@ toml = { version = "0.8.11" }
tracing = { version = "0.1.40" }
tracing-flame = { version = "0.2.0" }
tracing-indicatif = { version = "0.3.6" }
tracing-log = { version = "0.2.0" }
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"env-filter",
"fmt",
@@ -327,9 +326,3 @@ github-custom-job-permissions = { "build-docker" = { packages = "write", content
install-updater = false
# Path that installers should place binaries in
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
# Temporarily allow changes to the `release` workflow, in which we pin actions
# to a SHA instead of a tag (https://github.com/astral-sh/uv/issues/12253)
allow-dirty = ["ci"]
[workspace.metadata.dist.github-custom-runners]
global = "depot-ubuntu-latest-4"

View File

@@ -149,8 +149,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.11.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.10/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.1
rev: v0.9.10
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,61 +0,0 @@
# Running `mypy_primer`
## Basics
For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
```sh
uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h
```
Alternatively, you can install the forked version of `mypy_primer` using:
```sh
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
```
and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]).
## Showing the diagnostics diff between two Git revisions
To show the diagnostics diff between two Git revisions (e.g. your feature branch and `main`), run:
```sh
mypy_primer \
--type-checker knot \
--old origin/main \
--new my/feature \
--debug \
--output concise \
--project-selector '/black$'
```
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`.
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration
option to work correctly.
## Avoiding recompilation
If you want to run `mypy_primer` repeatedly, e.g. for different projects, but for the same combination of `--old` and `--new`, you
can use set the `MYPY_PRIMER_NO_REBUILD` environment variable to avoid recompilation of Red Knot:
```sh
MYPY_PRIMER_NO_REBUILD=1 mypy_primer …
```
## Running from a local copy of the repository
If you are working on a local branch, you can use `mypy_primer`'s `--repo` option to specify the path to your local copy of the `ruff` repository.
This allows `mypy_primer` to check out local branches:
```sh
mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch …
```
Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly.
[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support
[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py
[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables

View File

@@ -50,8 +50,6 @@ pub(crate) struct CheckCommand {
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
///
/// If not specified, Red Knot will look at the `VIRTUAL_ENV` environment variable.
///
/// Red Knot will search in the path's `site-packages` directories for type information and
/// third-party imports.
///
@@ -77,14 +75,6 @@ pub(crate) struct CheckCommand {
#[clap(flatten)]
pub(crate) rules: RulesArg,
/// The format to use for printing diagnostic messages.
#[arg(long)]
pub(crate) output_format: Option<OutputFormat>,
/// Control when colored output is used.
#[arg(long, value_name = "WHEN")]
pub(crate) color: Option<TerminalColor>,
/// Use exit code 1 if there are any warning-level diagnostics.
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
pub(crate) error_on_warning: Option<bool>,
@@ -127,9 +117,6 @@ impl CheckCommand {
..EnvironmentOptions::default()
}),
terminal: Some(TerminalOptions {
output_format: self
.output_format
.map(|output_format| RangedValue::cli(output_format.into())),
error_on_warning: self.error_on_warning,
}),
rules,
@@ -224,46 +211,3 @@ impl clap::Args for RulesArg {
Self::augment_args(cmd)
}
}
/// The diagnostic output format.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum OutputFormat {
/// Print diagnostics verbosely, with context and helpful hints.
///
/// Diagnostic messages may include additional context and
/// annotations on the input to help understand the message.
#[default]
#[value(name = "full")]
Full,
/// Print diagnostics concisely, one per line.
///
/// This will guarantee that each diagnostic is printed on
/// a single line. Only the most important or primary aspects
/// of the diagnostic are included. Contextual information is
/// dropped.
#[value(name = "concise")]
Concise,
}
impl From<OutputFormat> for ruff_db::diagnostic::DiagnosticFormat {
fn from(format: OutputFormat) -> ruff_db::diagnostic::DiagnosticFormat {
match format {
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
}
}
}
/// Control when colored output is used.
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub(crate) enum TerminalColor {
/// Display colors if the output goes to an interactive terminal.
#[default]
Auto,
/// Always display colors.
Always,
/// Never display colors.
Never,
}

View File

@@ -4,7 +4,7 @@ use std::process::{ExitCode, Termination};
use anyhow::Result;
use std::sync::Mutex;
use crate::args::{Args, CheckCommand, Command, TerminalColor};
use crate::args::{Args, CheckCommand, Command};
use crate::logging::setup_tracing;
use anyhow::{anyhow, Context};
use clap::Parser;
@@ -76,14 +76,10 @@ pub(crate) fn version() -> Result<()> {
}
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
set_colored_override(args.color);
let verbosity = args.verbosity.level();
countme::enable(verbosity.is_trace());
let _guard = setup_tracing(verbosity)?;
tracing::debug!("Version: {}", version::version());
// The base path to which all CLI arguments are relative to.
let cwd = {
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
@@ -259,16 +255,15 @@ impl MainLoop {
result,
revision: check_revision,
} => {
let terminal_settings = db.project().settings(db).terminal();
let display_config = DisplayDiagnosticConfig::default()
.format(terminal_settings.output_format)
.color(colored::control::SHOULD_COLORIZE.should_colorize());
let min_error_severity = if terminal_settings.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let min_error_severity =
if db.project().settings(db).terminal().error_on_warning {
Severity::Warning
} else {
Severity::Error
};
if check_revision == revision {
if db.project().files(db).is_empty() {
@@ -365,21 +360,3 @@ enum MainLoopMessage {
ApplyChanges(Vec<watch::ChangeEvent>),
Exit,
}
fn set_colored_override(color: Option<TerminalColor>) {
let Some(color) = color else {
return;
};
match color {
TerminalColor::Auto => {
colored::control::unset_override();
}
TerminalColor::Always => {
colored::control::set_override(true);
}
TerminalColor::Never => {
colored::control::set_override(false);
}
}
}

View File

@@ -86,7 +86,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
return a + b
a + b
"#,
),
(
@@ -158,7 +158,7 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
return a + b
a + b
"#,
),
(
@@ -219,7 +219,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| - Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -244,7 +244,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
@@ -306,7 +306,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
7 | x = a
8 |
9 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| - Name `x` used when possibly not defined
|
Found 3 diagnostics
@@ -331,7 +331,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ------------- Cannot resolve import `does_not_exit`
3 |
4 | y = 4 / 0
|
@@ -342,7 +342,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ----- Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, y):
|
@@ -393,7 +393,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| - Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -419,7 +419,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
@@ -456,7 +456,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
2 | [tool.knot.rules]
3 | division-by-zer = "warn" # incorrect rule name
| ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer`
| --------------- Unknown lint rule `division-by-zer`
|
Found 1 diagnostic
@@ -498,7 +498,7 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| - Name `x` used when not defined
|
Found 1 diagnostic
@@ -528,7 +528,7 @@ fn exit_code_only_info() -> anyhow::Result<()> {
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| ^^^^^^^^^^^^^^ Revealed type is `Literal[1]`
| -------------- info: Revealed type is `Literal[1]`
|
Found 1 diagnostic
@@ -558,7 +558,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| ^^^^^^^^^^^^^^ Revealed type is `Literal[1]`
| -------------- info: Revealed type is `Literal[1]`
|
Found 1 diagnostic
@@ -581,7 +581,7 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| - Name `x` used when not defined
|
Found 1 diagnostic
@@ -613,7 +613,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| - Name `x` used when not defined
|
Found 1 diagnostic
@@ -642,7 +642,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
@@ -680,7 +680,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
@@ -718,7 +718,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
@@ -778,7 +778,7 @@ fn user_configuration() -> anyhow::Result<()> {
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
@@ -789,7 +789,7 @@ fn user_configuration() -> anyhow::Result<()> {
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| - Name `x` used when possibly not defined
|
Found 2 diagnostics
@@ -820,7 +820,7 @@ fn user_configuration() -> anyhow::Result<()> {
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
@@ -967,30 +967,6 @@ fn check_non_existing_path() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn concise_diagnostics() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r"
success: false
exit_code: 1
----- stdout -----
warning[lint:unresolved-reference] <temp_dir>/test.py:2:7: Name `x` used when not defined
error[lint:non-subscriptable] <temp_dir>/test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
Found 2 diagnostics
----- stderr -----
");
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,

View File

@@ -1,3 +1,5 @@
#![allow(clippy::disallowed_names)]
use std::collections::HashSet;
use std::io::Write;
use std::time::{Duration, Instant};
@@ -14,7 +16,7 @@ use ruff_db::source::source_text;
use ruff_db::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
};
use ruff_db::{Db as _, Upcast};
use ruff_db::Upcast;
use ruff_python_ast::PythonVersion;
struct TestCase {
@@ -1788,82 +1790,3 @@ fn changes_to_user_configuration() -> anyhow::Result<()> {
Ok(())
}
/// Tests that renaming a file from `lib.py` to `Lib.py` is correctly reflected.
///
/// This test currently fails on case-insensitive systems because `Files` is case-sensitive
/// but the `System::metadata` call isn't. This means that
/// Red Knot considers both `Lib.py` and `lib.py` to exist when only `lib.py` does
///
/// The incoming change events then are no-ops because they don't change either file's
/// status nor does it update their last modified time (renaming a file doesn't bump it's
/// last modified timestamp).
///
/// Fixing this requires to either make `Files` case-insensitive and store the
/// real-case path (if it differs) on `File` or make `Files` use a
/// case-sensitive `System::metadata` call. This does open the question if all
/// `System` calls should be case sensitive. This would be the most consistent
/// but might be hard to pull off.
///
/// What the right solution is also depends on if Red Knot itself should be case
/// sensitive or not. E.g. should `include="src"` be case sensitive on all systems
/// or only on case-sensitive systems?
///
/// Lastly, whatever solution we pick must also work well with VS Code which,
/// unfortunately ,doesn't propagate casing-only renames.
/// <https://github.com/rust-lang/rust-analyzer/issues/9581>
#[ignore]
#[test]
fn rename_files_casing_only() -> anyhow::Result<()> {
let mut case = setup([("lib.py", "class Foo: ...")])?;
assert!(
resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
"Expected `lib` module to exist."
);
assert_eq!(
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
None,
"Expected `Lib` module not to exist"
);
// Now rename `lib.py` to `Lib.py`
if case.db().system().case_sensitivity().is_case_sensitive() {
std::fs::rename(
case.project_path("lib.py").as_std_path(),
case.project_path("Lib.py").as_std_path(),
)
.context("Failed to rename `lib.py` to `Lib.py`")?;
} else {
// On case-insensitive file systems, renaming a file to a different casing is a no-op.
// Rename to a different name first
std::fs::rename(
case.project_path("lib.py").as_std_path(),
case.project_path("temp.py").as_std_path(),
)
.context("Failed to rename `lib.py` to `temp.py`")?;
std::fs::rename(
case.project_path("temp.py").as_std_path(),
case.project_path("Lib.py").as_std_path(),
)
.context("Failed to rename `temp.py` to `Lib.py`")?;
}
let changes = case.stop_watch(event_for_file("Lib.py"));
case.apply_changes(changes);
// Resolving `lib` should now fail but `Lib` should now succeed
assert_eq!(
resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
None,
"Expected `lib` module to no longer exist."
);
assert!(
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
"Expected `Lib` module to exist"
);
Ok(())
}

View File

@@ -59,8 +59,9 @@ impl ProjectDatabase {
self.with_db(|db| db.project().check(db))
}
#[tracing::instrument(level = "debug", skip(self))]
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
self.with_db(|db| self.project().check_file(db, file))
}

View File

@@ -195,8 +195,7 @@ impl Project {
let project_span = project_span.clone();
scope.spawn(move |_| {
let check_file_span =
tracing::debug_span!(parent: &project_span, "check_file", ?file);
let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", file=%file.path(&db));
let _entered = check_file_span.entered();
let file_diagnostics = check_file_impl(&db, file);
@@ -326,7 +325,7 @@ impl Project {
self.files(db).contains(&file)
}
#[tracing::instrument(level = "debug", skip(self, db))]
#[tracing::instrument(level = "debug", skip(db))]
pub fn remove_file(self, db: &mut dyn Db, file: File) {
tracing::debug!(
"Removing file `{}` from project `{}`",

View File

@@ -2,7 +2,7 @@ use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSou
use crate::Db;
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
use ruff_db::diagnostic::{DiagnosticFormat, DiagnosticId, OldDiagnosticTrait, Severity, Span};
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, Severity, Span};
use ruff_db::files::system_path_to_file;
use ruff_db::system::{System, SystemPath};
use ruff_macros::Combine;
@@ -106,14 +106,9 @@ impl Options {
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
python_path: python
.map(|python_path| {
PythonPath::from_cli_flag(python_path.absolute(project_root, system))
PythonPath::SysPrefix(python_path.absolute(project_root, system))
})
.or_else(|| {
std::env::var("VIRTUAL_ENV")
.ok()
.map(PythonPath::from_virtual_env_var)
})
.unwrap_or_else(|| PythonPath::KnownSitePackages(vec![])),
.unwrap_or(PythonPath::KnownSitePackages(vec![])),
}
}
@@ -125,11 +120,6 @@ impl Options {
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {
output_format: terminal
.output_format
.as_deref()
.copied()
.unwrap_or_default(),
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
});
}
@@ -287,11 +277,6 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TerminalOptions {
/// The format to use for printing diagnostic messages.
///
/// Defaults to `full`.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<RangedValue<DiagnosticFormat>>,
/// Use exit code 1 if there are any warning-level diagnostics.
///
/// Defaults to `false`.

View File

@@ -1,7 +1,6 @@
use std::sync::Arc;
use red_knot_python_semantic::lint::RuleSelection;
use ruff_db::diagnostic::DiagnosticFormat;
/// The resolved [`super::Options`] for the project.
///
@@ -50,6 +49,5 @@ impl Settings {
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TerminalSettings {
pub output_format: DiagnosticFormat,
pub error_on_warning: bool,
}

View File

@@ -279,4 +279,23 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
/// Whether or not the .py/.pyi version of this file is expected to fail
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[];
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
// related to circular references in nested functions
("crates/ruff_linter/resources/test/fixtures/flake8_return/RET503.py", false, true),
// related to circular references in class definitions
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
// related to circular references in type aliases (salsa cycle panic):
("crates/ruff_python_parser/resources/inline/err/type_alias_invalid_value_expr.py", true, true),
("crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008.py", true, true),
// related to circular references in f-string annotations (invalid syntax)
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true),
// related to circular references in stub type annotations (salsa cycle panic):
("crates/ruff_linter/resources/test/fixtures/pycodestyle/E501_4.py", false, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_0.py", false, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_12.py", false, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_14.py", false, true),
];

View File

@@ -29,7 +29,7 @@ It is invalid to parameterize `Annotated` with less than two arguments.
```py
from typing_extensions import Annotated
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
def _(x: Annotated):
reveal_type(x) # revealed: Unknown
@@ -39,11 +39,11 @@ def _(flag: bool):
else:
X = bool
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
def f(y: X):
reveal_type(y) # revealed: Unknown | bool
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
def _(x: Annotated | bool):
reveal_type(x) # revealed: Unknown | bool

View File

@@ -47,10 +47,8 @@ def _(c: Callable[42, str]):
Or, when one of the parameter type is invalid in the list:
```py
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
# error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
def _(c: Callable[[int, 42, str, False], None]):
# revealed: (int, Unknown, str, Unknown, /) -> None
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
reveal_type(c)
```
@@ -63,7 +61,7 @@ from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int, str]]):
reveal_type(c) # revealed: (...) -> Unknown
reveal_type(c) # revealed: (int, str, /) -> Unknown
```
Or, an ellipsis:
@@ -74,18 +72,6 @@ def _(c: Callable[...]):
reveal_type(c) # revealed: (...) -> Unknown
```
Or something else that's invalid in a type expression generally:
```py
# fmt: off
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
{1, 2} # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
]
):
reveal_type(c) # revealed: (...) -> Unknown
```
### More than two arguments
We can't reliably infer the callable type if there are more then 2 arguments because we don't know
@@ -99,48 +85,6 @@ def _(c: Callable[[int], str, str]):
reveal_type(c) # revealed: (...) -> Unknown
```
### List as the second argument
```py
from typing import Callable
# fmt: off
def _(c: Callable[
int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
[str] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
]
):
reveal_type(c) # revealed: (...) -> Unknown
```
### List as both arguments
```py
from typing import Callable
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
def _(c: Callable[[int], [str]]):
reveal_type(c) # revealed: (int, /) -> Unknown
```
### Three list arguments
```py
from typing import Callable
# fmt: off
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
[int],
[str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
[bytes] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
]
):
reveal_type(c) # revealed: (...) -> Unknown
```
## Simple
A simple `Callable` with multiple parameters and a return type:
@@ -248,20 +192,4 @@ def _(c: Callable[[int, Unpack[Ts]], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
```
## Member lookup
```py
from typing import Callable
def _(c: Callable[[int], int]):
reveal_type(c.__init__) # revealed: Literal[__init__]
reveal_type(c.__class__) # revealed: type
# TODO: The member lookup for `Callable` uses `object` which does not have a `__call__`
# attribute. We could special case `__call__` in this context. Refer to
# https://github.com/astral-sh/ruff/pull/16493#discussion_r1985098508 for more details.
# error: [unresolved-attribute] "Type `(int, /) -> int` has no attribute `__call__`"
reveal_type(c.__call__) # revealed: Unknown
```
[gradual form]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-gradual-form

View File

@@ -38,8 +38,7 @@ If `__future__.annotations` is imported, annotations *are* deferred.
```py
from __future__ import annotations
def get_foo() -> Foo:
return Foo()
def get_foo() -> Foo: ...
class Foo: ...

View File

@@ -43,66 +43,3 @@ def _(
reveal_type(q) # revealed: Unknown
reveal_type(r) # revealed: Unknown
```
## Invalid AST nodes
```py
def bar() -> None:
return None
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(generics)
```
## Invalid Collection based AST nodes
```py
def _(
a: {1: 2}, # error: [invalid-type-form] "Dict literals are not allowed in type expressions"
b: {1, 2}, # error: [invalid-type-form] "Set literals are not allowed in type expressions"
c: {k: v for k, v in [(1, 2)]}, # error: [invalid-type-form] "Dict comprehensions are not allowed in type expressions"
d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions"
e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions"
f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions"
g: [int, str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
```

View File

@@ -127,13 +127,6 @@ Literal: _SpecialForm
```py
from other import Literal
# TODO: can we add a subdiagnostic here saying something like:
#
# `other.Literal` and `typing.Literal` have similar names, but are different symbols and don't have the same semantics
#
# ?
#
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
a1: Literal[26]
def f():
@@ -156,7 +149,7 @@ def f():
```py
from typing import Literal
# error: [invalid-type-form] "`typing.Literal` requires at least one argument when used in a type expression"
# error: [invalid-type-form] "`Literal` requires at least one argument when used in a type expression"
def _(x: Literal):
reveal_type(x) # revealed: Unknown
```

View File

@@ -45,13 +45,3 @@ def f():
# revealed: int | None
reveal_type(a)
```
## Invalid
```py
from typing import Optional
# error: [invalid-type-form] "`typing.Optional` requires exactly one argument when used in a type expression"
def f(x: Optional) -> None:
reveal_type(x) # revealed: Unknown
```

View File

@@ -59,13 +59,3 @@ def f():
# revealed: int | str
reveal_type(a)
```
## Invalid
```py
from typing import Union
# error: [invalid-type-form] "`typing.Union` requires at least one argument when used in a type expression"
def f(x: Union) -> None:
reveal_type(x) # revealed: Unknown
```

View File

@@ -18,7 +18,7 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
# TODO: should understand the annotation
reveal_type(args) # revealed: tuple
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
reveal_type(Alias) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`)
def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...
@@ -35,26 +35,7 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
class Foo:
def method(self, x: Self):
reveal_type(x) # revealed: @Todo(Support for `typing.Self`)
```
## Type expressions
One thing that is supported is error messages for using special forms in type expressions.
```py
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate
def _(
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
) -> None:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(x) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`)
```
## Inheritance

View File

@@ -1,6 +1,6 @@
# Unsupported type qualifiers
## Not yet fully supported
## Not yet supported
Several type qualifiers are unsupported by red-knot currently. However, we also don't emit
false-positive errors if you use one in an annotation:
@@ -19,33 +19,6 @@ class Bar(TypedDict):
z: ReadOnly[bytes]
```
## Type expressions
One thing that is supported is error messages for using type qualifiers in type expressions.
```py
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
def _(
a: (
Final # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
| int
),
b: (
ClassVar # error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
| int
),
c: Required, # error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
d: NotRequired, # error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
e: ReadOnly, # error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
) -> None:
reveal_type(a) # revealed: Unknown | int
reveal_type(b) # revealed: Unknown | int
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: Unknown
```
## Inheritance
You can't inherit from a type qualifier.

View File

@@ -10,10 +10,6 @@ reveal_type(x) # revealed: Literal[2]
x = 1.0
x /= 2
reveal_type(x) # revealed: int | float
x = (1, 2)
x += (3, 4)
reveal_type(x) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
```
## Dunder methods
@@ -165,18 +161,3 @@ def f(flag: bool, flag2: bool):
reveal_type(f) # revealed: int | str | float
```
## Implicit dunder calls on class objects
```py
class Meta(type):
def __iadd__(cls, other: int) -> str:
return ""
class C(metaclass=Meta): ...
cls = C
cls += 1
reveal_type(cls) # revealed: str
```

View File

@@ -348,11 +348,8 @@ reveal_type(C().y) # revealed: Unknown | str
```py
class ContextManager:
def __enter__(self) -> int | None:
return 1
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
def __enter__(self) -> int | None: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
class C:
def __init__(self) -> None:
@@ -368,11 +365,8 @@ reveal_type(c_instance.x) # revealed: Unknown | int | None
```py
class ContextManager:
def __enter__(self) -> tuple[int | None, int]:
return 1, 2
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
def __enter__(self) -> tuple[int | None, int]: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
class C:
def __init__(self) -> None:
@@ -808,7 +802,7 @@ def _(flag1: bool, flag2: bool):
reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
```
## Unions of attributes
## Union of attributes
If the (meta)class is a union type or if the attribute on the (meta) class has a union type, we
infer those union types accordingly:
@@ -818,74 +812,77 @@ def _(flag: bool):
if flag:
class C1:
x = 1
y: int = 1
else:
class C1:
x = 2
y: int | str = "b"
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
reveal_type(C1.y) # revealed: int | str
C1.y = 100
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `Literal[C1, C1]`"
C1.y = "problematic"
class C2:
if flag:
x = 3
y: int = 3
else:
x = 4
y: int | str = "d"
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
reveal_type(C2.y) # revealed: int | str
C2.y = 100
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
C2.y = None
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
C2.y = "problematic"
if flag:
class Meta3(type):
x = 5
y: int = 5
else:
class Meta3(type):
x = 6
y: int | str = "f"
class C3(metaclass=Meta3): ...
reveal_type(C3.x) # revealed: Unknown | Literal[5, 6]
reveal_type(C3.y) # revealed: int | str
C3.y = 100
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
C3.y = None
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
C3.y = "problematic"
class Meta4(type):
if flag:
x = 7
y: int = 7
else:
x = 8
y: int | str = "h"
class C4(metaclass=Meta4): ...
reveal_type(C4.x) # revealed: Unknown | Literal[7, 8]
reveal_type(C4.y) # revealed: int | str
```
C4.y = 100
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
C4.y = None
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
C4.y = "problematic"
## Inherited class attributes
### Basic
```py
class A:
X = "foo"
class B(A): ...
class C(B): ...
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
```
### Multiple inheritance
```py
class O: ...
class F(O):
X = 56
class E(O):
X = 42
class D(O): ...
class C(D, F): ...
class B(E, D): ...
class A(B, C): ...
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
reveal_type(A.X) # revealed: Unknown | Literal[42]
```
## Unions with possibly unbound paths
@@ -909,14 +906,8 @@ def _(flag1: bool, flag2: bool):
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `Literal[C1, C2, C3]`"
C.x = 100
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`"
C().x = 100
```
### Possibly-unbound within a class
@@ -941,16 +932,10 @@ def _(flag: bool, flag1: bool, flag2: bool):
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-unbound-attribute]
C.x = 100
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
# see the "Possibly unbound/undeclared instance attribute" section below.
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-unbound-attribute]
C().x = 100
```
### Possibly-unbound within gradual types
@@ -968,9 +953,6 @@ def _(flag: bool):
x: int
reveal_type(Derived().x) # revealed: int | Any
Derived().x = 1
Derived().x = "a"
```
### Attribute possibly unbound on a subclass but not on a superclass
@@ -985,10 +967,8 @@ def _(flag: bool):
x = 2
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
Bar.x = 3
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
Bar().x = 3
```
### Attribute possibly unbound on a subclass and on a superclass
@@ -1006,14 +986,8 @@ def _(flag: bool):
# error: [possibly-unbound-attribute]
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
# error: [possibly-unbound-attribute]
Bar.x = 3
# error: [possibly-unbound-attribute]
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
# error: [possibly-unbound-attribute]
Bar().x = 3
```
### Possibly unbound/undeclared instance attribute
@@ -1032,9 +1006,6 @@ def _(flag: bool):
# error: [possibly-unbound-attribute]
reveal_type(Foo().x) # revealed: int | Unknown
# error: [possibly-unbound-attribute]
Foo().x = 1
```
#### Possibly unbound
@@ -1049,229 +1020,9 @@ def _(flag: bool):
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
# if we ever will (or want to)
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
# Same here
Foo().x = 2
```
### Unions with all paths unbound
If the symbol is unbound in all elements of the union, we detect that:
```py
def _(flag: bool):
class C1: ...
class C2: ...
C = C1 if flag else C2
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
# TODO: This should ideally be a `unresolved-attribute` error. We need better union
# handling in `validate_attribute_assignment` for this.
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type `Literal[C1, C2]`"
C.x = 1
```
## Inherited class attributes
### Basic
```py
class A:
X = "foo"
class B(A): ...
class C(B): ...
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
C.X = "bar"
```
### Multiple inheritance
```py
class O: ...
class F(O):
X = 56
class E(O):
X = 42
class D(O): ...
class C(D, F): ...
class B(E, D): ...
class A(B, C): ...
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
reveal_type(A.X) # revealed: Unknown | Literal[42]
A.X = 100
```
## Intersections of attributes
### Attribute only available on one element
```py
from knot_extensions import Intersection
class A:
x: int = 1
class B: ...
def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.x) # revealed: int
a_and_b.x = 2
# Same for class objects
def _(a_and_b: Intersection[type[A], type[B]]):
reveal_type(a_and_b.x) # revealed: int
a_and_b.x = 2
```
### Attribute available on both elements
```py
from knot_extensions import Intersection
class P: ...
class Q: ...
class R(P, Q): ...
class A:
x: P = P()
class B:
x: Q = Q()
def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.x) # revealed: P & Q
a_and_b.x = R()
# Same for class objects
def _(a_and_b: Intersection[type[A], type[B]]):
reveal_type(a_and_b.x) # revealed: P & Q
a_and_b.x = R()
```
### Possible unboundness
```py
from knot_extensions import Intersection
class P: ...
class Q: ...
class R(P, Q): ...
def _(flag: bool):
class A1:
if flag:
x: P = P()
class B1: ...
def inner1(a_and_b: Intersection[A1, B1]):
# error: [possibly-unbound-attribute]
reveal_type(a_and_b.x) # revealed: P
# error: [possibly-unbound-attribute]
a_and_b.x = R()
# Same for class objects
def inner1_class(a_and_b: Intersection[type[A1], type[B1]]):
# error: [possibly-unbound-attribute]
reveal_type(a_and_b.x) # revealed: P
# error: [possibly-unbound-attribute]
a_and_b.x = R()
class A2:
if flag:
x: P = P()
class B1:
x: Q = Q()
def inner2(a_and_b: Intersection[A2, B1]):
reveal_type(a_and_b.x) # revealed: P & Q
# TODO: this should not be an error, we need better intersection
# handling in `validate_attribute_assignment` for this
# error: [possibly-unbound-attribute]
a_and_b.x = R()
# Same for class objects
def inner2_class(a_and_b: Intersection[type[A2], type[B1]]):
reveal_type(a_and_b.x) # revealed: P & Q
class A3:
if flag:
x: P = P()
class B3:
if flag:
x: Q = Q()
def inner3(a_and_b: Intersection[A3, B3]):
# error: [possibly-unbound-attribute]
reveal_type(a_and_b.x) # revealed: P & Q
# error: [possibly-unbound-attribute]
a_and_b.x = R()
# Same for class objects
def inner3_class(a_and_b: Intersection[type[A3], type[B3]]):
# error: [possibly-unbound-attribute]
reveal_type(a_and_b.x) # revealed: P & Q
# error: [possibly-unbound-attribute]
a_and_b.x = R()
class A4: ...
class B4: ...
def inner4(a_and_b: Intersection[A4, B4]):
# error: [unresolved-attribute]
reveal_type(a_and_b.x) # revealed: Unknown
# error: [invalid-assignment]
a_and_b.x = R()
# Same for class objects
def inner4_class(a_and_b: Intersection[type[A4], type[B4]]):
# error: [unresolved-attribute]
reveal_type(a_and_b.x) # revealed: Unknown
# error: [invalid-assignment]
a_and_b.x = R()
```
### Intersection of implicit instance attributes
```py
from knot_extensions import Intersection
class P: ...
class Q: ...
class A:
def __init__(self):
self.x: P = P()
class B:
def __init__(self):
self.x: Q = Q()
def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.x) # revealed: P & Q
```
## Attribute access on `Any`
### Attribute access on `Any`
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on
@@ -1302,96 +1053,18 @@ reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A
reveal_type(C.x) # revealed: Literal[1] & Any
```
## Classes with custom `__getattr__` methods
### Unions with all paths unbound
### Basic
If a type provides a custom `__getattr__` method, we use the return type of that method as the type
for unknown attributes. Consider the following `CustomGetAttr` class:
If the symbol is unbound in all elements of the union, we detect that:
```py
from typing import Literal
def _(flag: bool):
class C1: ...
class C2: ...
C = C1 if flag else C2
def flag() -> bool:
return True
class GetAttrReturnType: ...
class CustomGetAttr:
class_attr: int = 1
if flag():
possibly_unbound: bytes = b"a"
def __init__(self) -> None:
self.instance_attr: str = "a"
def __getattr__(self, name: str) -> GetAttrReturnType:
return GetAttrReturnType()
```
We can access arbitrary attributes on instances of this class, and the type of the attribute will be
`GetAttrReturnType`:
```py
c = CustomGetAttr()
reveal_type(c.whatever) # revealed: GetAttrReturnType
```
If an attribute is defined on the class, it takes precedence over the `__getattr__` method:
```py
reveal_type(c.class_attr) # revealed: int
```
If the class attribute is possibly unbound, we union the type of the attribute with the fallback
type of the `__getattr__` method:
```py
reveal_type(c.possibly_unbound) # revealed: bytes | GetAttrReturnType
```
Instance attributes also take precedence over the `__getattr__` method:
```py
# Note: we could attempt to union with the fallback type of `__getattr__` here, as we currently do not
# attempt to determine if instance attributes are always bound or not. Neither mypy nor pyright do this,
# so it's not a priority.
reveal_type(c.instance_attr) # revealed: str
```
### Type of the `name` parameter
If the `name` parameter of the `__getattr__` method is annotated with a (union of) literal type(s),
we only consider the attribute access to be valid if the accessed attribute is one of them:
```py
from typing import Literal
class Date:
def __getattr__(self, name: Literal["day", "month", "year"]) -> int:
return 0
date = Date()
reveal_type(date.day) # revealed: int
reveal_type(date.month) # revealed: int
reveal_type(date.year) # revealed: int
# error: [unresolved-attribute] "Type `Date` has no attribute `century`"
reveal_type(date.century) # revealed: Unknown
```
### `argparse.Namespace`
A standard library example of a class with a custom `__getattr__` method is `argparse.Namespace`:
```py
import argparse
def _(ns: argparse.Namespace):
reveal_type(ns.whatever) # revealed: Any
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
```
## Objects of all types have a `__class__` method
@@ -1446,8 +1119,6 @@ reveal_type(Foo.__class__) # revealed: Literal[type]
## Module attributes
### Basic
`mod.py`:
```py
@@ -1482,7 +1153,7 @@ for mod.global_symbol in IntIterable():
pass
```
### Nested module attributes
## Nested attributes
`outer/__init__.py`:

View File

@@ -363,7 +363,7 @@ reveal_type(X() + Y()) # revealed: int
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
a = NotBoolable()
@@ -406,12 +406,10 @@ A left-hand dunder method doesn't apply for the right-hand operand, or vice vers
```py
class A:
def __add__(self, other) -> int:
return 1
def __add__(self, other) -> int: ...
class B:
def __radd__(self, other) -> int:
return 1
def __radd__(self, other) -> int: ...
class C: ...

View File

@@ -1,22 +0,0 @@
# Binary operations on tuples
## Concatenation for heterogeneous tuples
```py
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]]
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]]
reveal_type(() + ()) # revealed: tuple[()]
def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]]
reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str]
```
## Concatenation for homogeneous tuples
```py
def _(x: tuple[int, ...], y: tuple[str, ...]):
reveal_type(x + y) # revealed: @Todo(full tuple[...] support)
reveal_type(x + (1, 2)) # revealed: @Todo(full tuple[...] support)
```

View File

@@ -1,51 +0,0 @@
# Binary operations on union types
Binary operations on union types are only available if they are supported for all possible
combinations of types:
```py
def f1(i: int, u: int | None):
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int` and `int | None`"
reveal_type(i + u) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | None` and `int`"
reveal_type(u + i) # revealed: Unknown
```
`int` can be added to `int`, and `str` can be added to `str`, but expressions of type `int | str`
cannot be added, because that would require addition of `int` and `str` or vice versa:
```py
def f2(i: int, s: str, int_or_str: int | str):
i + i
s + s
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
reveal_type(int_or_str + int_or_str) # revealed: Unknown
```
However, if an operation is supported for all possible combinations, the result will be a union of
the possible outcomes:
```py
from typing import Literal
def f3(two_or_three: Literal[2, 3], a_or_b: Literal["a", "b"]):
reveal_type(two_or_three + two_or_three) # revealed: Literal[4, 5, 6]
reveal_type(two_or_three**two_or_three) # revealed: Literal[4, 8, 9, 27]
reveal_type(a_or_b + a_or_b) # revealed: Literal["aa", "ab", "ba", "bb"]
reveal_type(two_or_three * a_or_b) # revealed: Literal["aa", "bb", "aaa", "bbb"]
```
We treat a type annotation of `float` as a union of `int` and `float`, so union handling is relevant
here:
```py
def f4(x: float, y: float):
reveal_type(x + y) # revealed: int | float
reveal_type(x - y) # revealed: int | float
reveal_type(x * y) # revealed: int | float
reveal_type(x / y) # revealed: int | float
reveal_type(x // y) # revealed: int | float
reveal_type(x % y) # revealed: int | float
```

View File

@@ -69,8 +69,7 @@ without raising an error.
from typing import Any
def any() -> Any: ...
def flag() -> bool:
return True
def flag() -> bool: ...
a: int
b: str
@@ -127,8 +126,7 @@ inferred types:
from typing import Any
def any() -> Any: ...
def flag() -> bool:
return True
def flag() -> bool: ...
a = 1
b = 2
@@ -166,8 +164,7 @@ error for both `a` and `b`:
```py
from typing import Any
def flag() -> bool:
return True
def flag() -> bool: ...
if flag():
a: Any = 1
@@ -197,8 +194,7 @@ seems inconsistent when compared to the case just above.
`mod.py`:
```py
def flag() -> bool:
return True
def flag() -> bool: ...
if flag():
a: int
@@ -252,8 +248,7 @@ inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" cas
`mod.py`:
```py
def flag() -> bool:
return True
def flag() -> bool: ...
if flag:
a = 1

View File

@@ -6,7 +6,7 @@
class NotBool:
__bool__ = None
# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2"
# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument.
bool(1, 2)
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
@@ -29,12 +29,9 @@ But a three-argument call to type creates a dynamic instance of the `type` class
reveal_type(type("Foo", (), {})) # revealed: type
```
Other numbers of arguments are invalid
Other numbers of arguments are invalid (TODO -- these should emit a diagnostic)
```py
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
```

View File

@@ -25,8 +25,7 @@ reveal_type(b) # revealed: Unknown
def _(flag: bool):
class PossiblyNotCallable:
if flag:
def __call__(self) -> int:
return 1
def __call__(self) -> int: ...
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
@@ -39,8 +38,7 @@ def _(flag: bool):
def _(flag: bool):
if flag:
class PossiblyUnbound:
def __call__(self) -> int:
return 1
def __call__(self) -> int: ...
# error: [possibly-unresolved-reference]
a = PossiblyUnbound()
@@ -66,12 +64,11 @@ def _(flag: bool):
if flag:
__call__ = 1
else:
def __call__(self) -> int:
return 1
def __call__(self) -> int: ...
a = NonCallable()
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(a()) # revealed: Unknown | int
reveal_type(a()) # revealed: int | Unknown
```
## Call binding errors

View File

@@ -185,7 +185,7 @@ def _(flag: bool):
return str(key)
else:
def __getitem__(self, key: int) -> bytes:
return bytes()
return key
c = C()
reveal_type(c[0]) # revealed: str | bytes
@@ -198,7 +198,7 @@ def _(flag: bool):
else:
class D:
def __getitem__(self, key: int) -> bytes:
return bytes()
return key
d = D()
reveal_type(d[0]) # revealed: str | bytes

View File

@@ -37,8 +37,6 @@ def foo() -> int:
return 42
def decorator(func) -> Callable[[], int]:
# TODO: no error
# error: [invalid-return-type]
return foo
@decorator

View File

@@ -235,23 +235,23 @@ method_wrapper(C(), None)
method_wrapper(None, C)
# Passing `None` without an `owner` argument is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
# error: [missing-argument] "No argument provided for required parameter `owner`"
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
method_wrapper(None, None)
# Calling `__get__` without any arguments is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
# error: [missing-argument] "No argument provided for required parameter `instance`"
method_wrapper()
# Calling `__get__` with too many positional arguments is an
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3"
method_wrapper(C(), C, "one too many")
```

View File

@@ -40,7 +40,7 @@ def _(flag: bool):
def f() -> int:
return 1
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(x) # revealed: Unknown | int
reveal_type(x) # revealed: int | Unknown
```
## Multiple non-callable elements in a union
@@ -58,7 +58,7 @@ def _(flag: bool, flag2: bool):
return 1
# TODO we should mention all non-callable elements of the union
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
# revealed: Unknown | int
# revealed: int | Unknown
reveal_type(f())
```
@@ -82,12 +82,8 @@ def _(flag: bool):
Calling a union where the arguments don't match the signature of all variants.
```py
def f1(a: int) -> int:
return a
def f2(a: str) -> str:
return a
def f1(a: int) -> int: ...
def f2(a: str) -> str: ...
def _(flag: bool):
if flag:
f = f1
@@ -148,16 +144,3 @@ def _(flag: bool):
x = f(3)
reveal_type(x) # revealed: Unknown
```
## Union including a special-cased function
```py
def _(flag: bool):
if flag:
f = str
else:
f = repr
reveal_type(str("string")) # revealed: Literal["string"]
reveal_type(repr("string")) # revealed: Literal["'string'"]
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
```

View File

@@ -191,7 +191,7 @@ It may also be more appropriate to use `unsupported-operator` as the error code.
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
class WithContains:
def __contains__(self, item) -> NotBoolable:

View File

@@ -154,7 +154,7 @@ reveal_type(B() >= A()) # revealed: LeReturnType
class C:
def __gt__(self, other: C) -> EqReturnType:
return EqReturnType()
return 42
def __ge__(self, other: C) -> NeReturnType:
return NeReturnType()
@@ -355,7 +355,7 @@ element) of a chained comparison.
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
class Comparable:
def __lt__(self, item) -> NotBoolable:

View File

@@ -110,8 +110,7 @@ given operator:
```py
class Container:
def __contains__(self, x) -> bool:
return False
def __contains__(self, x) -> bool: ...
class NonContainer: ...
@@ -131,8 +130,7 @@ unsupported for the given operator:
```py
class Container:
def __contains__(self, x) -> bool:
return False
def __contains__(self, x) -> bool: ...
class NonContainer: ...

View File

@@ -22,19 +22,14 @@ Walking through examples:
from __future__ import annotations
class A:
def __lt__(self, other) -> A:
return self
def __gt__(self, other) -> bool:
return False
def __lt__(self, other) -> A: ...
def __gt__(self, other) -> bool: ...
class B:
def __lt__(self, other) -> B:
return self
def __lt__(self, other) -> B: ...
class C:
def __lt__(self, other) -> C:
return self
def __lt__(self, other) -> C: ...
x = A() < B() < C()
reveal_type(x) # revealed: A & ~AlwaysTruthy | B

View File

@@ -197,7 +197,7 @@ class LtReturnTypeOnB: ...
class B:
def __lt__(self, o: B) -> LtReturnTypeOnB:
return LtReturnTypeOnB()
return set()
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
```
@@ -355,7 +355,7 @@ def compute_chained_comparison():
```py
class NotBoolable:
__bool__: int = 5
__bool__ = 5
class Comparable:
def __lt__(self, other) -> NotBoolable:
@@ -387,7 +387,7 @@ class A:
return NotBoolable()
class NotBoolable:
__bool__: None = None
__bool__ = None
# error: [unsupported-bool-conversion]
(A(),) == (A(),)

View File

@@ -104,8 +104,7 @@ class Iterator:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
def __iter__(self) -> Iterator: ...
# This is fine:
x = [*Iterable()]

View File

@@ -40,7 +40,7 @@ def _(flag: bool):
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
3 if NotBoolable() else 4

View File

@@ -152,7 +152,7 @@ def _(flag: bool):
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():

View File

@@ -48,7 +48,7 @@ def _(target: int):
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
def _(target: int, flag: NotBoolable):
y = 1

View File

@@ -6,9 +6,7 @@ A descriptor is an attribute value that has one of the methods in the descriptor
methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an
attribute, it is said to be a descriptor.
## Basic properties
### Example
## Basic example
An introductory example, modeled after a [simple example] in the primer on descriptors, involving a
descriptor that returns a constant value:
@@ -32,26 +30,18 @@ reveal_type(c.ten) # revealed: Literal[10]
reveal_type(C.ten) # revealed: Literal[10]
# This is fine:
# These are fine:
c.ten = 10
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `ten` on type `C` with custom `__set__` method"
c.ten = 11
```
When assigning to the `ten` attribute from the class object, we get an error. The descriptor
protocol is *not* triggered in this case. Since the attribute is declared as `Ten` in the class
body, we do not allow these assignments, preventing users from accidentally overwriting the data
descriptor, which is what would happen at runtime:
```py
# error: [invalid-assignment] "Object of type `Literal[10]` is not assignable to attribute `ten` of type `Ten`"
C.ten = 10
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
c.ten = 11
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
C.ten = 11
```
### Different types for `__get__` and `__set__`
## Different types for `__get__` and `__set__`
The return type of `__get__` and the value type of `__set__` can be different:
@@ -74,17 +64,19 @@ c = C()
reveal_type(c.flexible_int) # revealed: int | None
c.flexible_int = 42 # okay
# TODO: This should not be an error
# error: [invalid-assignment]
c.flexible_int = "42" # also okay!
reveal_type(c.flexible_int) # revealed: int | None
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `flexible_int` on type `C` with custom `__set__` method"
# TODO: This should be an error
c.flexible_int = None # not okay
reveal_type(c.flexible_int) # revealed: int | None
```
### Data and non-data descriptors
## Data and non-data descriptors
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example of a
data descriptor is a `property` with a setter and/or a deleter. Descriptors that only define
@@ -139,84 +131,6 @@ reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
C.data_descriptor = "something else" # This is okay
```
### Partial fall back
Our implementation of the descriptor protocol takes into account that symbols can be possibly
unbound. In those cases, we fall back to lower precedence steps of the descriptor protocol and union
all possible results accordingly. We start by defining a data and a non-data descriptor:
```py
from typing import Literal
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: object, value: int) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
return "non-data"
```
Then, we demonstrate that we fall back to an instance attribute if a data descriptor is possibly
unbound:
```py
def f1(flag: bool):
class C1:
if flag:
attr = DataDescriptor()
def f(self):
self.attr = "normal"
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
# Assigning to the attribute also causes no `possibly-unbound` diagnostic:
C1().attr = 1
```
We never treat implicit instance attributes as definitely bound, so we fall back to the non-data
descriptor here:
```py
class C2:
def f(self):
self.attr = "normal"
attr = NonDataDescriptor()
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
# Assignments always go to the instance attribute in this case
C2().attr = 1
```
### Descriptors only work when used as class variables
Descriptors only work when used as class variables. When put in instances, they have no effect.
```py
from typing import Literal
class Ten:
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
return 10
class C:
def __init__(self):
self.ten: Ten = Ten()
reveal_type(C().ten) # revealed: Ten
C().ten = Ten()
# The instance attribute is declared as `Ten`, so this is an
# error: [invalid-assignment] "Object of type `Literal[10]` is not assignable to attribute `ten` of type `Ten`"
C().ten = 10
```
## Descriptor protocol for class objects
When attributes are accessed on a class object, the following [precedence chain] is used:
@@ -236,7 +150,7 @@ class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: object, value: int) -> None:
def __set__(self, instance: object, value: str) -> None:
pass
class NonDataDescriptor:
@@ -263,28 +177,7 @@ reveal_type(C1.class_data_descriptor) # revealed: Literal["data"]
reveal_type(C1.class_non_data_descriptor) # revealed: Literal["non-data"]
```
Assignments to class object attribute only trigger the descriptor protocol if the data descriptor is
on the metaclass:
```py
C1.meta_data_descriptor = 1
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `Literal[C1]` with custom `__set__` method"
C1.meta_data_descriptor = "invalid"
```
When writing to a class-level data descriptor from the class object itself, the descriptor protocol
is *not* triggered (this is in contrast to what happens when you read class-level descriptor
attributes!). So the following assignment does not call `__set__`. At runtime, the assignment would
overwrite the data descriptor, but the attribute is declared as `DataDescriptor` in the class body,
so we do not allow this:
```py
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `class_data_descriptor` of type `DataDescriptor`"
C1.class_data_descriptor = 1
```
We now demonstrate that a *metaclass data descriptor* takes precedence over all class-level
Next, we demonstrate that a *metaclass data descriptor* takes precedence over all class-level
attributes:
```py
@@ -305,14 +198,6 @@ class C2(metaclass=Meta2):
reveal_type(C2.meta_data_descriptor1) # revealed: Literal["data"]
reveal_type(C2.meta_data_descriptor2) # revealed: Literal["data"]
C2.meta_data_descriptor1 = 1
C2.meta_data_descriptor2 = 1
# error: [invalid-assignment]
C2.meta_data_descriptor1 = "invalid"
# error: [invalid-assignment]
C2.meta_data_descriptor2 = "invalid"
```
On the other hand, normal metaclass attributes and metaclass non-data descriptors are shadowed by
@@ -367,16 +252,6 @@ def _(flag: bool):
reveal_type(C5.meta_data_descriptor1) # revealed: Literal["data", "value on class"]
# error: [possibly-unbound-attribute]
reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"]
# TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not
# wrong, but they could be subsumed under a higher-level diagnostic.
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `Literal[C5]` with custom `__set__` method"
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`"
C5.meta_data_descriptor1 = None
# error: [possibly-unbound-attribute]
C5.meta_data_descriptor2 = 1
```
When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the
@@ -429,71 +304,58 @@ def _(flag: bool):
reveal_type(C7.union_of_metaclass_data_descriptor_and_attribute) # revealed: Literal["data", 2]
reveal_type(C7.union_of_class_attributes) # revealed: Literal[1, 2]
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
C7.union_of_metaclass_attributes = 2 if flag else 1
C7.union_of_metaclass_data_descriptor_and_attribute = 2 if flag else 100
C7.union_of_class_attributes = 2 if flag else 1
C7.union_of_class_data_descriptor_and_attribute = 2 if flag else DataDescriptor()
```
## Descriptors distinguishing between class and instance access
## Partial fall back
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
when it is accessed on an instance. A real-world example of this is the `__get__` method on
`types.FunctionType`.
Our implementation of the descriptor protocol takes into account that symbols can be possibly
unbound. In those cases, we fall back to lower precedence steps of the descriptor protocol and union
all possible results accordingly. We start by defining a data and a non-data descriptor:
```py
from typing_extensions import Literal, LiteralString, overload
from typing import Literal
class Descriptor:
@overload
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
@overload
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
def __get__(self, instance, owner=None, /) -> LiteralString:
if instance:
return "called on instance"
else:
return "called on class object"
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
class C:
d: Descriptor = Descriptor()
def __set__(self, instance: object, value: int) -> None:
pass
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: LiteralString
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: LiteralString
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
return "non-data"
```
## Descriptor protocol for dunder methods
Dunder methods are always looked up on the meta-type. There is no instance fallback. This means that
an implicit dunder call on an instance-like object will not only look up the dunder method on the
class object, without considering instance attributes. And an implicit dunder call on a class object
will look up the dunder method on the metaclass, without considering class attributes.
Then, we demonstrate that we fall back to an instance attribute if a data descriptor is possibly
unbound:
```py
class SomeCallable:
def __call__(self, x: int) -> str:
return "a"
def f1(flag: bool):
class C1:
if flag:
attr = DataDescriptor()
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
return SomeCallable()
def f(self):
self.attr = "normal"
class B:
__call__: Descriptor = Descriptor()
b_instance = B()
reveal_type(b_instance(1)) # revealed: str
b_instance("bla") # error: [invalid-argument-type]
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
```
## Special descriptors
We never treat implicit instance attributes as definitely bound, so we fall back to the non-data
descriptor here:
### Built-in `property` descriptor
```py
def f2(flag: bool):
class C2:
def f(self):
self.attr = "normal"
attr = NonDataDescriptor()
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
```
## Built-in `property` descriptor
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
determined by the return type of the `name` method and the parameter type of the setter,
@@ -530,11 +392,11 @@ c.name = "new"
c.name = None
# TODO: this should be an error, but with a proper error message
# error: [invalid-assignment] "Implicit shadowing of function `name`; annotate to make it explicit if this is intentional"
# error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to attribute `name` of type `<bound method `name` of `C`>`"
c.name = 42
```
### Built-in `classmethod` descriptor
## Built-in `classmethod` descriptor
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first
argument to the class instead of the instance.
@@ -561,7 +423,244 @@ reveal_type(C.get_name()) # revealed: str
reveal_type(C("42").get_name()) # revealed: str
```
### Functions as descriptors
## Descriptors only work when used as class variables
From the descriptor guide:
> Descriptors only work when used as class variables. When put in instances, they have no effect.
```py
from typing import Literal
class Ten:
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
return 10
class C:
def __init__(self):
self.ten: Ten = Ten()
reveal_type(C().ten) # revealed: Ten
```
## Descriptors distinguishing between class and instance access
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
when it is accessed on an instance. A real-world example of this is the `__get__` method on
`types.FunctionType`.
```py
from typing_extensions import Literal, LiteralString, overload
class Descriptor:
@overload
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
@overload
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
def __get__(self, instance, owner=None, /) -> LiteralString:
if instance:
return "called on instance"
else:
return "called on class object"
class C:
d: Descriptor = Descriptor()
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: LiteralString
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: LiteralString
```
## Undeclared descriptor arguments
If a descriptor attribute is not declared, we union with `Unknown`, just like for regular
attributes, since that attribute could be overwritten externally. Even a data descriptor with a
`__set__` method can be overwritten when accessed through a class object.
```py
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: object, value: int) -> None:
pass
class C:
descriptor = Descriptor()
C.descriptor = "something else"
# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments
reveal_type(C.descriptor) # revealed: Unknown | int
```
## `__get__` is called with correct arguments
```py
from __future__ import annotations
class TailoredForClassObjectAccess:
def __get__(self, instance: None, owner: type[C]) -> int:
return 1
class TailoredForInstanceAccess:
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
return "a"
class TailoredForMetaclassAccess:
def __get__(self, instance: type[C], owner: type[Meta]) -> bytes:
return b"a"
class Meta(type):
metaclass_access: TailoredForMetaclassAccess = TailoredForMetaclassAccess()
class C(metaclass=Meta):
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
reveal_type(C.class_object_access) # revealed: int
reveal_type(C().instance_access) # revealed: str
reveal_type(C.metaclass_access) # revealed: bytes
# TODO: These should emit a diagnostic
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
```
## Descriptors with incorrect `__get__` signature
```py
class Descriptor:
# `__get__` method with missing parameters:
def __get__(self) -> int:
return 1
class C:
descriptor: Descriptor = Descriptor()
# TODO: This should be an error
reveal_type(C.descriptor) # revealed: Descriptor
# TODO: This should be an error
reveal_type(C().descriptor) # revealed: Descriptor
```
## Possibly unbound descriptor attributes
```py
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: int, value) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def _(flag: bool):
class PossiblyUnbound:
if flag:
non_data: NonDataDescriptor = NonDataDescriptor()
data: DataDescriptor = DataDescriptor()
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
reveal_type(PossiblyUnbound.non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
reveal_type(PossiblyUnbound().non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
reveal_type(PossiblyUnbound.data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
reveal_type(PossiblyUnbound().data) # revealed: int
```
## Possibly-unbound `__get__` method
```py
def _(flag: bool):
class MaybeDescriptor:
if flag:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
class C:
descriptor: MaybeDescriptor = MaybeDescriptor()
reveal_type(C.descriptor) # revealed: int | MaybeDescriptor
reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
```
## Descriptors with non-function `__get__` callables that are descriptors themselves
The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the
descriptor protocol on the callable's `__call__` method:
```py
from __future__ import annotations
class ReturnedCallable2:
def __call__(self, descriptor: Descriptor1, instance: None, owner: type[C]) -> int:
return 1
class ReturnedCallable1:
def __call__(self, descriptor: Descriptor2, instance: Callable1, owner: type[Callable1]) -> ReturnedCallable2:
return ReturnedCallable2()
class Callable3:
def __call__(self, descriptor: Descriptor3, instance: Callable2, owner: type[Callable2]) -> ReturnedCallable1:
return ReturnedCallable1()
class Descriptor3:
__get__: Callable3 = Callable3()
class Callable2:
__call__: Descriptor3 = Descriptor3()
class Descriptor2:
__get__: Callable2 = Callable2()
class Callable1:
__call__: Descriptor2 = Descriptor2()
class Descriptor1:
__get__: Callable1 = Callable1()
class C:
d: Descriptor1 = Descriptor1()
reveal_type(C.d) # revealed: int
```
## Dunder methods
Dunder methods are looked up on the meta-type, but we still need to invoke the descriptor protocol:
```py
class SomeCallable:
def __call__(self, x: int) -> str:
return "a"
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
return SomeCallable()
class B:
__call__: Descriptor = Descriptor()
b_instance = B()
reveal_type(b_instance(1)) # revealed: str
b_instance("bla") # error: [invalid-argument-type]
```
## Functions as descriptors
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
that method calls work as expected. See [this test suite](./call/methods.md) for more information.
@@ -611,202 +710,33 @@ Finally, we test some error cases for the call to the wrapper descriptor:
```py
# Calling the wrapper descriptor without any arguments is an
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`"
wrapper_descriptor()
# Calling it without the `instance` argument is an also an
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
# error: [missing-argument] "No argument provided for required parameter `instance`"
wrapper_descriptor(f)
# Calling it without the `owner` argument if `instance` is not `None` is an
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
# error: [missing-argument] "No argument provided for required parameter `owner`"
wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
wrapper_descriptor(f, C())
# Calling it with something that is not a `FunctionType` as the first argument is an
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`"
wrapper_descriptor(1, None, type(f))
# Calling it with something that is not a `type` as the `owner` argument is an
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type`"
wrapper_descriptor(f, None, f)
# Calling it with too many positional arguments is an
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4"
wrapper_descriptor(f, None, type(f), "one too many")
```
## Error handling and edge cases
### `__get__` is called with correct arguments
This test makes sure that we call `__get__` with the right argument types for various scenarios:
```py
from __future__ import annotations
class TailoredForClassObjectAccess:
def __get__(self, instance: None, owner: type[C]) -> int:
return 1
class TailoredForInstanceAccess:
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
return "a"
class TailoredForMetaclassAccess:
def __get__(self, instance: type[C], owner: type[Meta]) -> bytes:
return b"a"
class Meta(type):
metaclass_access: TailoredForMetaclassAccess = TailoredForMetaclassAccess()
class C(metaclass=Meta):
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
reveal_type(C.class_object_access) # revealed: int
reveal_type(C().instance_access) # revealed: str
reveal_type(C.metaclass_access) # revealed: bytes
# TODO: These should emit a diagnostic
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
```
### Descriptors with incorrect `__get__` signature
```py
class Descriptor:
# `__get__` method with missing parameters:
def __get__(self) -> int:
return 1
class C:
descriptor: Descriptor = Descriptor()
# TODO: This should be an error
reveal_type(C.descriptor) # revealed: Descriptor
# TODO: This should be an error
reveal_type(C().descriptor) # revealed: Descriptor
```
### Undeclared descriptor arguments
If a descriptor attribute is not declared, we union with `Unknown`, just like for regular
attributes, since that attribute could be overwritten externally. Even a data descriptor with a
`__set__` method can be overwritten when accessed through a class object.
```py
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: object, value: int) -> None:
pass
class C:
descriptor = Descriptor()
C.descriptor = "something else"
# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments
reveal_type(C.descriptor) # revealed: Unknown | int
```
### Possibly unbound descriptor attributes
```py
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: int, value) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def _(flag: bool):
class PossiblyUnbound:
if flag:
non_data: NonDataDescriptor = NonDataDescriptor()
data: DataDescriptor = DataDescriptor()
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
reveal_type(PossiblyUnbound.non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
reveal_type(PossiblyUnbound().non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
reveal_type(PossiblyUnbound.data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
reveal_type(PossiblyUnbound().data) # revealed: int
```
### Possibly-unbound `__get__` method
```py
def _(flag: bool):
class MaybeDescriptor:
if flag:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
class C:
descriptor: MaybeDescriptor = MaybeDescriptor()
reveal_type(C.descriptor) # revealed: int | MaybeDescriptor
reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
```
### Descriptors with non-function `__get__` callables that are descriptors themselves
The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the
descriptor protocol on the callable's `__call__` method:
```py
from __future__ import annotations
class ReturnedCallable2:
def __call__(self, descriptor: Descriptor1, instance: None, owner: type[C]) -> int:
return 1
class ReturnedCallable1:
def __call__(self, descriptor: Descriptor2, instance: Callable1, owner: type[Callable1]) -> ReturnedCallable2:
return ReturnedCallable2()
class Callable3:
def __call__(self, descriptor: Descriptor3, instance: Callable2, owner: type[Callable2]) -> ReturnedCallable1:
return ReturnedCallable1()
class Descriptor3:
__get__: Callable3 = Callable3()
class Callable2:
__call__: Descriptor3 = Descriptor3()
class Descriptor2:
__get__: Callable2 = Callable2()
class Callable1:
__call__: Descriptor2 = Descriptor2()
class Descriptor1:
__get__: Callable1 = Callable1()
class C:
d: Descriptor1 = Descriptor1()
reveal_type(C.d) # revealed: int
```
[descriptors]: https://docs.python.org/3/howto/descriptor.html
[precedence chain]: https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L5393-L5481
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant

View File

@@ -1,149 +0,0 @@
# Attribute assignment
<!-- snapshot-diagnostics -->
This test suite demonstrates various kinds of diagnostics that can be emitted in a
`obj.attr = value` assignment.
## Instance attributes with class-level defaults
These can be set on instances and on class objects.
```py
class C:
attr: int = 0
instance = C()
instance.attr = 1 # fine
instance.attr = "wrong" # error: [invalid-assignment]
C.attr = 1 # fine
C.attr = "wrong" # error: [invalid-assignment]
```
## Pure instance attributes
These can only be set on instances. When trying to set them on class objects, we generate a useful
diagnostic that mentions that the attribute is only available on instances.
```py
class C:
def __init__(self):
self.attr: int = 0
instance = C()
instance.attr = 1 # fine
instance.attr = "wrong" # error: [invalid-assignment]
C.attr = 1 # error: [invalid-attribute-access]
```
## `ClassVar`s
These can only be set on class objects. When trying to set them on instances, we generate a useful
diagnostic that mentions that the attribute is only available on class objects.
```py
from typing import ClassVar
class C:
attr: ClassVar[int] = 0
C.attr = 1 # fine
C.attr = "wrong" # error: [invalid-assignment]
instance = C()
instance.attr = 1 # error: [invalid-attribute-access]
```
## Unknown attributes
When trying to set an attribute that is not defined, we also emit errors:
```py
class C: ...
C.non_existent = 1 # error: [unresolved-attribute]
instance = C()
instance.non_existent = 1 # error: [unresolved-attribute]
```
## Possibly-unbound attributes
When trying to set an attribute that is not defined in all branches, we emit errors:
```py
def _(flag: bool) -> None:
class C:
if flag:
attr: int = 0
C.attr = 1 # error: [possibly-unbound-attribute]
instance = C()
instance.attr = 1 # error: [possibly-unbound-attribute]
```
## Data descriptors
When assigning to a data descriptor attribute, we implicitly call the descriptor's `__set__` method.
This can lead to various kinds of diagnostics.
### Invalid argument type
```py
class Descriptor:
def __set__(self, instance: object, value: int) -> None:
pass
class C:
attr: Descriptor = Descriptor()
instance = C()
instance.attr = 1 # fine
# TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
instance.attr = "wrong" # error: [invalid-assignment]
```
### Invalid `__set__` method signature
```py
class WrongDescriptor:
def __set__(self, instance: object, value: int, extra: int) -> None:
pass
class C:
attr: WrongDescriptor = WrongDescriptor()
instance = C()
# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
instance.attr = 1 # error: [invalid-assignment]
```
## Setting attributes on union types
```py
def _(flag: bool) -> None:
if flag:
class C1:
attr: int = 0
else:
class C1:
attr: str = ""
# TODO: The error message here could be improved to explain why the assignment fails.
C1.attr = 1 # error: [invalid-assignment]
class C2:
if flag:
attr: int = 0
else:
attr: str = ""
# TODO: This should be an error
C2.attr = 1
```

View File

@@ -1,14 +0,0 @@
# No matching overload diagnostics
<!-- snapshot-diagnostics -->
## Calls to overloaded functions
TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in
real Python code. We are instead testing a special-cased function where we create an overloaded
signature internally. Update this to an `@overload` function in the Python snippet itself once we
can.
```py
type("Foo", ()) # error: [no-matching-overload]
```

View File

@@ -16,10 +16,8 @@ reveal_type(cast(int | str, 1)) # revealed: int | str
# error: [invalid-type-form]
reveal_type(cast(Literal, True)) # revealed: Unknown
# error: [invalid-type-form]
reveal_type(cast(1, True)) # revealed: Unknown
# TODO: These should be errors
cast(1)
cast(str)
cast(str, b"ar", "foo")

View File

@@ -2,7 +2,7 @@
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
assert NotBoolable()

View File

@@ -121,7 +121,7 @@ if NotBoolable():
```py
class NotBoolable:
__bool__: None = None
__bool__ = None
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
@@ -133,9 +133,9 @@ if NotBoolable():
```py
def test(cond: bool):
class NotBoolable:
__bool__: int | None = None if cond else 3
__bool__ = None if cond else 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`"
if NotBoolable():
...
```
@@ -145,7 +145,7 @@ def test(cond: bool):
```py
def test(cond: bool):
class NotBoolable:
__bool__: None = None
__bool__ = None
a = 10 if cond else NotBoolable()

View File

@@ -5,10 +5,10 @@
`lambda` expressions can be defined without any parameters.
```py
reveal_type(lambda: 1) # revealed: () -> Unknown
reveal_type(lambda: 1) # revealed: () -> @Todo(lambda return type)
# error: [unresolved-reference]
reveal_type(lambda: a) # revealed: () -> Unknown
reveal_type(lambda: a) # revealed: () -> @Todo(lambda return type)
```
## With parameters
@@ -17,45 +17,45 @@ Unlike parameters in function definition, the parameters in a `lambda` expressio
annotated.
```py
reveal_type(lambda a: a) # revealed: (a) -> Unknown
reveal_type(lambda a, b: a + b) # revealed: (a, b) -> Unknown
reveal_type(lambda a: a) # revealed: (a) -> @Todo(lambda return type)
reveal_type(lambda a, b: a + b) # revealed: (a, b) -> @Todo(lambda return type)
```
But, it can have default values:
```py
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> Unknown
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> Unknown
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> @Todo(lambda return type)
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> @Todo(lambda return type)
```
And, positional-only parameters:
```py
reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> Unknown
reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> @Todo(lambda return type)
```
And, keyword-only parameters:
```py
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> Unknown
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> @Todo(lambda return type)
```
And, variadic parameter:
```py
reveal_type(lambda *args: args) # revealed: (*args) -> Unknown
reveal_type(lambda *args: args) # revealed: (*args) -> @Todo(lambda return type)
```
And, keyword-varidic parameter:
```py
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> Unknown
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> @Todo(lambda return type)
```
Mixing all of them together:
```py
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> Unknown
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> @Todo(lambda return type)
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
```
@@ -96,5 +96,5 @@ Here, a `lambda` expression is used as the default value for a parameter in anot
expression.
```py
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> @Todo(lambda return type)) -> @Todo(lambda return type)
```

View File

@@ -99,28 +99,22 @@ The returned value of `__len__` is implicitly and recursively converted to `int`
from typing import Literal
class Zero:
def __len__(self) -> Literal[0]:
return 0
def __len__(self) -> Literal[0]: ...
class ZeroOrOne:
def __len__(self) -> Literal[0, 1]:
return 0
def __len__(self) -> Literal[0, 1]: ...
class ZeroOrTrue:
def __len__(self) -> Literal[0, True]:
return 0
def __len__(self) -> Literal[0, True]: ...
class OneOrFalse:
def __len__(self) -> Literal[1] | Literal[False]:
return 1
def __len__(self) -> Literal[1] | Literal[False]: ...
class OneOrFoo:
def __len__(self) -> Literal[1, "foo"]:
return 1
def __len__(self) -> Literal[1, "foo"]: ...
class ZeroOrStr:
def __len__(self) -> Literal[0] | str:
return 0
def __len__(self) -> Literal[0] | str: ...
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
@@ -140,12 +134,10 @@ reveal_type(len(ZeroOrStr())) # revealed: int
from typing import Literal
class LiteralTrue:
def __len__(self) -> Literal[True]:
return True
def __len__(self) -> Literal[True]: ...
class LiteralFalse:
def __len__(self) -> Literal[False]:
return False
def __len__(self) -> Literal[False]: ...
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
@@ -165,24 +157,19 @@ class SomeEnum(Enum):
INT_2 = 3_2
class Auto:
def __len__(self) -> Literal[SomeEnum.AUTO]:
return SomeEnum.AUTO
def __len__(self) -> Literal[SomeEnum.AUTO]: ...
class Int:
def __len__(self) -> Literal[SomeEnum.INT]:
return SomeEnum.INT
def __len__(self) -> Literal[SomeEnum.INT]: ...
class Str:
def __len__(self) -> Literal[SomeEnum.STR]:
return SomeEnum.STR
def __len__(self) -> Literal[SomeEnum.STR]: ...
class Tuple:
def __len__(self) -> Literal[SomeEnum.TUPLE]:
return SomeEnum.TUPLE
def __len__(self) -> Literal[SomeEnum.TUPLE]: ...
class IntUnion:
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]:
return SomeEnum.INT
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
reveal_type(len(Auto())) # revealed: int
reveal_type(len(Int())) # revealed: int
@@ -197,8 +184,7 @@ reveal_type(len(IntUnion())) # revealed: int
from typing import Literal
class Negative:
def __len__(self) -> Literal[-1]:
return -1
def __len__(self) -> Literal[-1]: ...
# TODO: Emit a diagnostic
reveal_type(len(Negative())) # revealed: int
@@ -210,12 +196,10 @@ reveal_type(len(Negative())) # revealed: int
from typing import Literal
class SecondOptionalArgument:
def __len__(self, v: int = 0) -> Literal[0]:
return 0
def __len__(self, v: int = 0) -> Literal[0]: ...
class SecondRequiredArgument:
def __len__(self, v: int) -> Literal[1]:
return 1
def __len__(self, v: int) -> Literal[1]: ...
# TODO: Emit a diagnostic
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]

View File

@@ -1,245 +0,0 @@
# Function return type
When a function's return type is annotated, all return statements are checked to ensure that the
type of the returned value is assignable to the annotated return type.
## Basic examples
A return value assignable to the annotated return type is valid.
```py
def f() -> int:
return 1
```
The type of the value obtained by calling a function is the annotated return type, not the inferred
return type.
```py
reveal_type(f()) # revealed: int
```
A `raise` is equivalent to a return of `Never`, which is assignable to any annotated return type.
```py
def f() -> str:
raise ValueError()
reveal_type(f()) # revealed: str
```
## Stub functions
"Stub" function definitions (that is, function definitions with an empty body) are permissible in
stub files, or in a few other locations: Protocol method definitions, abstract methods, and
overloads. In this case the function body is considered to be omitted (thus no return type checking
is performed on it), not assumed to implicitly return `None`.
A stub function's "empty" body may contain only an optional docstring, followed (optionally) by an
ellipsis (`...`) or `pass`.
### In stub file
```pyi
def f() -> int: ...
def f() -> int:
pass
def f() -> int:
"""Some docstring"""
def f() -> int:
"""Some docstring"""
...
```
### In Protocol
```py
from typing import Protocol
class Bar(Protocol):
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
```
### In abstract method
```py
from abc import ABC, abstractmethod
class Foo(ABC):
@abstractmethod
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
@abstractmethod
# error: [invalid-return-type]
def g[T](self, x: T) -> T: ...
```
### In overload
```py
from typing import overload
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
def f(x: int | str):
return x
```
## Conditional return type
```py
def f(cond: bool) -> int:
if cond:
return 1
else:
return 2
def f(cond: bool) -> int | None:
if cond:
return 1
else:
return
def f(cond: bool) -> int:
if cond:
return 1
else:
raise ValueError()
def f(cond: bool) -> str | int:
if cond:
return "a"
else:
return 1
```
## Implicit return type
```py
def f(cond: bool) -> int | None:
if cond:
return 1
# no implicit return
def f() -> int:
if True:
return 1
# no implicit return
def f(cond: bool) -> int:
cond = True
if cond:
return 1
def f(cond: bool) -> int:
if cond:
cond = True
else:
return 1
if cond:
return 2
```
## Invalid return type
<!-- snapshot-diagnostics -->
```py
# error: [invalid-return-type]
def f() -> int:
1
def f() -> str:
# error: [invalid-return-type]
return 1
def f() -> int:
# error: [invalid-return-type]
return
from typing import TypeVar
T = TypeVar("T")
# TODO: `invalid-return-type` error should be emitted
def m(x: T) -> T: ...
```
## Invalid return type in stub file
<!-- snapshot-diagnostics -->
```pyi
def f() -> int:
# error: [invalid-return-type]
return ...
# error: [invalid-return-type]
def foo() -> int:
print("...")
...
# error: [invalid-return-type]
def foo() -> int:
f"""{foo} is a function that ..."""
...
```
## Invalid conditional return type
<!-- snapshot-diagnostics -->
```py
def f(cond: bool) -> str:
if cond:
return "a"
else:
# error: [invalid-return-type]
return 1
def f(cond: bool) -> str:
if cond:
# error: [invalid-return-type]
return 1
else:
# error: [invalid-return-type]
return 2
```
## Invalid implicit return type
<!-- snapshot-diagnostics -->
```py
def f() -> None:
if False:
# error: [invalid-return-type]
return 1
# error: [invalid-return-type]
def f(cond: bool) -> int:
if cond:
return 1
# error: [invalid-return-type]
def f(cond: bool) -> int:
if cond:
raise ValueError()
# error: [invalid-return-type]
def f(cond: bool) -> int:
if cond:
cond = False
else:
return 1
if cond:
return 2
```

View File

@@ -162,8 +162,6 @@ class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
A similar case can work in a non-stub file, if forward references are stringified:
`string_annotation.py`:
```py
@@ -176,25 +174,14 @@ class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
In a non-stub file, without stringified forward references, this raises a `NameError`:
`bare_annotation.py`:
```py
class Base[T]: ...
# TODO: error: [unresolved-reference]
# error: [non-subscriptable]
class Sub(Base[Sub]): ...
```
## Another cyclic case
```pyi
# TODO no error (generics)
# error: [invalid-base]
class Derived[T](list[Derived[T]]): ...
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View File

@@ -35,7 +35,7 @@ Each typevar must also appear _somewhere_ in the parameter list:
```py
def absurd[T]() -> T:
# There's no way to construct a T!
raise ValueError("absurd")
...
```
## Inferring generic function parameter types
@@ -48,8 +48,7 @@ whether we want to infer a more specific `Literal` type where possible, or use h
the inferred type to e.g. `int`.
```py
def f[T](x: T) -> T:
return x
def f[T](x: T) -> T: ...
# TODO: no error
# TODO: revealed: int or Literal[1]
@@ -78,8 +77,7 @@ The matching up of call arguments and discovery of constraints on typevars can b
process for arbitrarily-nested generic types in parameters.
```py
def f[T](x: list[T]) -> T:
return x[0]
def f[T](x: list[T]) -> T: ...
# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: T
@@ -121,7 +119,7 @@ def different_types[T, S](cond: bool, t: T, s: S) -> T:
if cond:
return t
else:
# error: [invalid-return-type] "Object of type `S` is not assignable to return type `T`"
# TODO: error: S is not assignable to T
return s
def same_types[T](cond: bool, t1: T, t2: T) -> T:

View File

@@ -37,11 +37,8 @@ from typing import TypeVar
T = TypeVar("T")
def f1(x: T) -> T:
return x
def f2(x: T) -> T:
return x
def f1(x: T) -> T: ...
def f2(x: T) -> T: ...
f1(1)
f2("a")
@@ -56,8 +53,7 @@ This also applies to a single generic function being used multiple times, instan
to a different type each time.
```py
def f[T](x: T) -> T:
return x
def f[T](x: T) -> T: ...
# TODO: no error
# TODO: revealed: int or Literal[1]
@@ -76,11 +72,8 @@ reveal_type(f("a")) # revealed: T
```py
class C[T]:
def m1(self, x: T) -> T:
return x
def m2(self, x: T) -> T:
return x
def m1(self, x: T) -> T: ...
def m2(self, x: T) -> T: ...
c: C[int] = C()
# TODO: no error
@@ -108,8 +101,7 @@ S = TypeVar("S")
# TODO: no error
# error: [invalid-base]
class Legacy(Generic[T]):
def m(self, x: T, y: S) -> S:
return y
def m(self, x: T, y: S) -> S: ...
legacy: Legacy[int] = Legacy()
# TODO: revealed: str
@@ -120,8 +112,7 @@ With PEP 695 syntax, it is clearer that the method uses a separate typevar:
```py
class C[T]:
def m[S](self, x: T, y: S) -> S:
return y
def m[S](self, x: T, y: S) -> S: ...
c: C[int] = C()
# TODO: no errors
@@ -156,8 +147,7 @@ class C(Generic[T]):
x: list[S] = []
# This is not an error, as shown in the previous test
def m(self, x: S) -> S:
return x
def m(self, x: S) -> S: ...
```
This is true with PEP 695 syntax, as well, though we must use the legacy syntax to define the
@@ -179,11 +169,8 @@ class C[T]:
# TODO: error
x: list[S] = []
def m1(self, x: S) -> S:
return x
def m2[S](self, x: S) -> S:
return x
def m1(self, x: S) -> S: ...
def m2[S](self, x: S) -> S: ...
```
## Nested formal typevars must be distinct

View File

@@ -140,27 +140,3 @@ import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```py
```
## Long paths
It's unlikely that a single module component is as long as in this example, but Windows treats paths
that are longer than 200 and something specially. This test ensures that Red Knot can handle those
paths gracefully.
```toml
system = "os"
```
`AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath/__init__.py`:
```py
class Foo: ...
```
```py
from AveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPathAveryLongPath import (
Foo,
)
reveal_type(Foo()) # revealed: Foo
```

View File

@@ -1,7 +1,10 @@
# Case Sensitive Imports
```toml
system = "os"
# TODO: This test should use the real file system instead of the memory file system.
# but we can't change the file system yet because the tests would then start failing for
# case-insensitive file systems.
#system = "os"
```
Python's import system is case-sensitive even on case-insensitive file system. This means, importing

View File

@@ -846,19 +846,5 @@ def mixed(
reveal_type(i4) # revealed: Any & Unknown
```
## Invalid
```py
from knot_extensions import Intersection, Not
# error: [invalid-type-form] "`knot_extensions.Intersection` requires at least one argument when used in a type expression"
def f(x: Intersection) -> None:
reveal_type(x) # revealed: Unknown
# error: [invalid-type-form] "`knot_extensions.Not` requires exactly one argument when used in a type expression"
def f(x: Not) -> None:
reveal_type(x) # revealed: Unknown
```
[complement laws]: https://en.wikipedia.org/wiki/Complement_(set_theory)
[de morgan's laws]: https://en.wikipedia.org/wiki/De_Morgan%27s_laws

View File

@@ -431,7 +431,7 @@ def _(flag: bool):
# invalid signature because it only accepts a `str`,
# but the old-style iteration protocol will pass it an `int`
def __getitem__(self, key: str) -> bytes:
return bytes()
return 42
# error: [not-iterable]
for x in Iterable():
@@ -484,7 +484,7 @@ def _(flag1: bool, flag2: bool):
return Iterator()
if flag2:
def __getitem__(self, key: int) -> bytes:
return bytes()
return 42
# error: [not-iterable]
for x in Iterable():
@@ -682,7 +682,7 @@ def _(flag: bool):
return "foo"
else:
def __getitem__(self, item: str) -> int:
return 42
return "foo"
# error: [not-iterable]
for x in Iterable1():
@@ -723,7 +723,7 @@ def _(flag: bool, flag2: bool):
return "foo"
else:
def __getitem__(self, item: str) -> int:
return 42
return "foo"
if flag2:
def __iter__(self) -> Iterator:
return Iterator()

View File

@@ -10,8 +10,7 @@ class Iterator:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
def __iter__(self) -> Iterator: ...
def generator_function():
yield from Iterable()

View File

@@ -121,7 +121,7 @@ def _(flag: bool, flag2: bool):
```py
class NotBoolable:
__bool__: int = 3
__bool__ = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
while NotBoolable():

View File

@@ -166,8 +166,7 @@ When a class has an explicit `metaclass` that is not a class, but is a callable
`type.__new__` arguments, we should return the meta-type of its return type.
```py
def f(*args, **kwargs) -> int:
return 1
def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...

View File

@@ -22,13 +22,11 @@ def _(flag: bool):
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2"
if bool(x is not None, 5):
if bool(x is not None, 5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
# error: [unknown-argument] "Argument `y` does not match any known parameter of class `bool`"
if bool(x is not None, y=5):
if bool(x is not None, y=5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -170,8 +170,7 @@ if issubclass(t, int):
def issubclass(c, ci):
return True
def flag() -> bool:
return True
def flag() -> bool: ...
t = int if flag() else str
if issubclass(t, int):
@@ -183,8 +182,7 @@ if issubclass(t, int):
```py
issubclass_alias = issubclass
def flag() -> bool:
return True
def flag() -> bool: ...
t = int if flag() else str
if issubclass_alias(t, int):
@@ -196,8 +194,7 @@ if issubclass_alias(t, int):
```py
from builtins import issubclass as imported_issubclass
def flag() -> bool:
return True
def flag() -> bool: ...
t = int if flag() else str
if imported_issubclass(t, int):
@@ -209,8 +206,7 @@ if imported_issubclass(t, int):
```py
from typing import Any
def flag() -> bool:
return True
def flag() -> bool: ...
t = int if flag() else str
@@ -233,8 +229,7 @@ if issubclass(t, Any):
### Do not narrow if there are keyword arguments
```py
def flag() -> bool:
return True
def flag() -> bool: ...
t = int if flag() else str

View File

@@ -20,8 +20,7 @@ def _(flag: bool):
## Class patterns
```py
def get_object() -> object:
return object()
def get_object() -> object: ...
class A: ...
class B: ...
@@ -43,12 +42,10 @@ reveal_type(x) # revealed: object
## Class pattern with guard
```py
def get_object() -> object:
return object()
def get_object() -> object: ...
class A:
def y() -> int:
return 1
def y() -> int: ...
class B: ...

View File

@@ -233,20 +233,16 @@ reveal_type(y) # revealed: A
from typing import Literal
class MetaAmbiguous(type):
def __bool__(self) -> bool:
return True
def __bool__(self) -> bool: ...
class MetaFalsy(type):
def __bool__(self) -> Literal[False]:
return False
def __bool__(self) -> Literal[False]: ...
class MetaTruthy(type):
def __bool__(self) -> Literal[True]:
return True
def __bool__(self) -> Literal[True]: ...
class MetaDeferred(type):
def __bool__(self) -> MetaAmbiguous:
return MetaAmbiguous()
def __bool__(self) -> MetaAmbiguous: ...
class AmbiguousClass(metaclass=MetaAmbiguous): ...
class FalsyClass(metaclass=MetaFalsy): ...
@@ -304,24 +300,20 @@ def _(x: bool | str):
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
class Falsy:
def __bool__(self) -> Literal[False]:
return False
def __bool__(self) -> Literal[False]: ...
class Truthy:
def __bool__(self) -> Literal[True]:
return True
def __bool__(self) -> Literal[True]: ...
def _(x: Falsy | Truthy):
reveal_type(x or A()) # revealed: Truthy | A
reveal_type(x and A()) # revealed: Falsy | A
class MetaFalsy(type):
def __bool__(self) -> Literal[False]:
return False
def __bool__(self) -> Literal[False]: ...
class MetaTruthy(type):
def __bool__(self) -> Literal[True]:
return True
def __bool__(self) -> Literal[True]: ...
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...

View File

@@ -88,7 +88,7 @@ def _(x: str | int):
```py
def _(x: str | int):
# error: [no-matching-overload] "No overload of class `type` matches arguments"
# TODO: we could issue a diagnostic here
if type(object=x) is str:
reveal_type(x) # revealed: str | int
```

View File

@@ -9,8 +9,7 @@ is retained after the loop.
## Basic `while` loop
```py
def next_item() -> int | None:
return 1
def next_item() -> int | None: ...
x = next_item()
@@ -24,8 +23,7 @@ reveal_type(x) # revealed: None
## `while` loop with `else`
```py
def next_item() -> int | None:
return 1
def next_item() -> int | None: ...
x = next_item()
@@ -43,8 +41,7 @@ reveal_type(x) # revealed: None
```py
from typing import Literal
def next_item() -> Literal[1, 2, 3]:
raise NotImplementedError
def next_item() -> Literal[1, 2, 3]: ...
x = next_item()

View File

@@ -13,13 +13,3 @@ def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex
b: SupportsIndex = some_literal_int
c: SupportsIndex = some_indexable
```
## Invalid
```py
from typing import Protocol
# error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
def f(x: Protocol) -> None:
reveal_type(x) # revealed: Unknown
```

View File

@@ -113,24 +113,6 @@ class D(B, A): ... # fine
class E(B, C, A): ... # fine
```
## Post-hoc modifications
```py
class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
class B:
__slots__ = ("c", "d")
class C(
A, # error: [incompatible-slots]
B, # error: [incompatible-slots]
): ...
```
## False negatives
### Possibly unbound
@@ -178,6 +160,22 @@ class B:
class C(A, B): ...
```
### Post-hoc modifications
```py
class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: @Todo(return type of decorated function)
class B:
__slots__ = ("c", "d")
# False negative: [incompatible-slots]
class C(A, B): ...
```
### Built-ins with implicit layouts
```py

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid `__set__` method signature
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | class WrongDescriptor:
2 | def __set__(self, instance: object, value: int, extra: int) -> None:
3 | pass
4 |
5 | class C:
6 | attr: WrongDescriptor = WrongDescriptor()
7 |
8 | instance = C()
9 |
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
11 | instance.attr = 1 # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:11:1
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
11 | instance.attr = 1 # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
|
```

View File

@@ -1,40 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Data descriptors - Invalid argument type
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | class Descriptor:
2 | def __set__(self, instance: object, value: int) -> None:
3 | pass
4 |
5 | class C:
6 | attr: Descriptor = Descriptor()
7 |
8 | instance = C()
9 | instance.attr = 1 # fine
10 |
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
12 | instance.attr = "wrong" # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:12:1
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
12 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
|
```

View File

@@ -1,51 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Instance attributes with class-level defaults
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C:
2 | attr: int = 0
3 |
4 | instance = C()
5 | instance.attr = 1 # fine
6 | instance.attr = "wrong" # error: [invalid-assignment]
7 |
8 | C.attr = 1 # fine
9 | C.attr = "wrong" # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:6:1
|
4 | instance = C()
5 | instance.attr = 1 # fine
6 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
7 |
8 | C.attr = 1 # fine
|
```
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:9:1
|
8 | C.attr = 1 # fine
9 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
|
```

View File

@@ -1,51 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | def _(flag: bool) -> None:
2 | class C:
3 | if flag:
4 | attr: int = 0
5 |
6 | C.attr = 1 # error: [possibly-unbound-attribute]
7 |
8 | instance = C()
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
```
# Diagnostics
```
warning: lint:possibly-unbound-attribute
--> /src/mdtest_snippet.py:6:5
|
4 | attr: int = 0
5 |
6 | C.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound
7 |
8 | instance = C()
|
```
```
warning: lint:possibly-unbound-attribute
--> /src/mdtest_snippet.py:9:5
|
8 | instance = C()
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound
|
```

View File

@@ -1,52 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Pure instance attributes
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C:
2 | def __init__(self):
3 | self.attr: int = 0
4 |
5 | instance = C()
6 | instance.attr = 1 # fine
7 | instance.attr = "wrong" # error: [invalid-assignment]
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:7:1
|
5 | instance = C()
6 | instance.attr = 1 # fine
7 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
|
```
```
error: lint:invalid-attribute-access
--> /src/mdtest_snippet.py:9:1
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
|
```

View File

@@ -1,50 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Setting attributes on union types
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | def _(flag: bool) -> None:
2 | if flag:
3 | class C1:
4 | attr: int = 0
5 |
6 | else:
7 | class C1:
8 | attr: str = ""
9 |
10 | # TODO: The error message here could be improved to explain why the assignment fails.
11 | C1.attr = 1 # error: [invalid-assignment]
12 |
13 | class C2:
14 | if flag:
15 | attr: int = 0
16 | else:
17 | attr: str = ""
18 |
19 | # TODO: This should be an error
20 | C2.attr = 1
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:11:5
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
11 | C1.attr = 1 # error: [invalid-assignment]
| ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
12 |
13 | class C2:
|
```

View File

@@ -1,48 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - Unknown attributes
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C: ...
2 |
3 | C.non_existent = 1 # error: [unresolved-attribute]
4 |
5 | instance = C()
6 | instance.non_existent = 1 # error: [unresolved-attribute]
```
# Diagnostics
```
error: lint:unresolved-attribute
--> /src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`.
4 |
5 | instance = C()
|
```
```
error: lint:unresolved-attribute
--> /src/mdtest_snippet.py:6:1
|
5 | instance = C()
6 | instance.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`.
|
```

View File

@@ -1,51 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attribute_assignment.md - Attribute assignment - `ClassVar`s
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import ClassVar
2 |
3 | class C:
4 | attr: ClassVar[int] = 0
5 |
6 | C.attr = 1 # fine
7 | C.attr = "wrong" # error: [invalid-assignment]
8 |
9 | instance = C()
10 | instance.attr = 1 # error: [invalid-attribute-access]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:7:1
|
6 | C.attr = 1 # fine
7 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
8 |
9 | instance = C()
|
```
```
error: lint:invalid-attribute-access
--> /src/mdtest_snippet.py:10:1
|
9 | instance = C()
10 | instance.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C`
|
```

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