Compare commits

..

1 Commits

Author SHA1 Message Date
Dhruv Manilawala
2b8c801a79 Make tracing aware that walkdir threads run as part of request 2025-01-16 14:09:16 +05:30
782 changed files with 16459 additions and 22036 deletions

View File

@@ -6,10 +6,3 @@ failure-output = "immediate-final"
fail-fast = false
status-level = "skip"
# Mark tests that take longer than 1s as slow.
# Terminate after 60s as a stop-gap measure to terminate on deadlock.
slow-timeout = { period = "1s", terminate-after = 60 }
# Show slow jobs in the final summary
final-status-level = "slow"

4
.gitattributes vendored
View File

@@ -14,7 +14,5 @@ crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text
crates/ruff_python_parser/resources/inline linguist-generated=true
ruff.schema.json -diff linguist-generated=true text=auto eol=lf
crates/ruff_python_ast/src/generated.rs -diff linguist-generated=true text=auto eol=lf
crates/ruff_python_formatter/src/generated.rs -diff linguist-generated=true text=auto eol=lf
ruff.schema.json linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown

View File

@@ -1,2 +0,0 @@
# This file cannot use the extension `.yaml`.
blank_issues_enabled: false

View File

@@ -1,22 +0,0 @@
name: New issue
description: A generic issue
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report an issue! We're glad to have you involved with Ruff.
If you're filing a bug report, please consider including the following information:
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
e.g. "RUF001", "unused variable", "Jupyter notebook"
* A minimal code snippet that reproduces the bug.
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
* The current Ruff settings (any relevant sections from your `pyproject.toml`).
* The current Ruff version (`ruff --version`).
- type: textarea
attributes:
label: Description
description: A description of the issue

View File

@@ -6,5 +6,4 @@ self-hosted-runner:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16
- windows-latest-xlarge

View File

@@ -23,8 +23,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
env:
PACKAGE_NAME: ruff
MODULE_NAME: ruff

View File

@@ -51,7 +51,7 @@ jobs:
env:
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
run: |
version=$(grep -m 1 "^version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${TAG}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${TAG}" >&2

View File

@@ -1,7 +1,5 @@
name: CI
permissions: {}
on:
push:
branches: [main]
@@ -61,7 +59,6 @@ jobs:
- Cargo.toml
- Cargo.lock
- crates/**
- "!crates/red_knot*/**"
- "!crates/ruff_python_formatter/**"
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
@@ -119,11 +116,11 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: |
rustup component add clippy
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- name: "Clippy (wasm)"
@@ -133,13 +130,12 @@ jobs:
name: "cargo test (linux)"
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -152,6 +148,7 @@ jobs:
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
@@ -179,13 +176,12 @@ jobs:
name: "cargo test (linux, release)"
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -198,6 +194,7 @@ jobs:
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
@@ -206,25 +203,24 @@ jobs:
cargo-test-windows:
name: "cargo test (windows)"
runs-on: github-windows-2025-x86_64-16
runs-on: windows-latest-xlarge
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
@@ -235,13 +231,12 @@ jobs:
name: "cargo test (wasm)"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
@@ -252,6 +247,7 @@ jobs:
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: Swatinem/rust-cache@v2
- name: "Test ruff_wasm"
run: |
cd crates/ruff_wasm
@@ -270,11 +266,11 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@v2
- name: "Build"
run: cargo build --release --locked
@@ -282,7 +278,7 @@ jobs:
name: "cargo build (msrv)"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -293,7 +289,6 @@ jobs:
with:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
env:
MSRV: ${{ steps.msrv.outputs.value }}
@@ -308,6 +303,7 @@ jobs:
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
env:
@@ -325,11 +321,11 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
with:
workspaces: "fuzz -> target"
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@main
with:
@@ -345,7 +341,7 @@ jobs:
needs:
- cargo-test-linux
- determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && needs.determine_changes.outputs.parser == 'true' }}
if: ${{ needs.determine_changes.outputs.parser == 'true' }}
timeout-minutes: 20
env:
FORCE_COLOR: 1
@@ -381,21 +377,15 @@ jobs:
name: "test scripts"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- 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
# already checked into git.
- run: python crates/ruff_python_ast/generate.py
- run: python crates/ruff_python_formatter/generate.py
- run: test -z "$(git status --porcelain)"
# Verify that adding a plugin or rule produces clean code.
- uses: Swatinem/rust-cache@v2
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
- run: cargo check
- run: cargo fmt --all --check
@@ -413,7 +403,7 @@ jobs:
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
# Ecosystem check needs linter and/or formatter changes.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
if: ${{ github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -547,7 +537,6 @@ jobs:
name: "python package"
runs-on: ubuntu-latest
timeout-minutes: 20
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
steps:
- uses: actions/checkout@v4
with:
@@ -582,9 +571,9 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: "Install pre-commit"
run: pip install pre-commit
- name: "Cache pre-commit"
@@ -616,7 +605,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@v2
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.9.0
@@ -626,6 +614,7 @@ jobs:
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@v5
- uses: Swatinem/rust-cache@v2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -649,15 +638,16 @@ jobs:
name: "formatter instabilities and black similarity"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }}
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
- name: "Cache rust"
uses: Swatinem/rust-cache@v2
- name: "Run checks"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
@@ -672,7 +662,7 @@ jobs:
needs:
- cargo-test-linux
- determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: extractions/setup-just@v2
env:
@@ -714,7 +704,7 @@ jobs:
benchmarks:
runs-on: ubuntu-22.04
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ github.repository == 'astral-sh/ruff' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -722,8 +712,6 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
@@ -732,6 +720,8 @@ jobs:
with:
tool: cargo-codspeed
- uses: Swatinem/rust-cache@v2
- name: "Build benchmarks"
run: cargo codspeed build --features codspeed -p ruff_benchmark

View File

@@ -49,7 +49,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.13.1
uses: cloudflare/wrangler-action@v3.13.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

7
.github/zizmor.yml vendored
View File

@@ -10,10 +10,3 @@ rules:
ignore:
- build-docker.yml
- publish-playground.yml
excessive-permissions:
# it's hard to test what the impact of removing these ignores would be
# without actually running the release workflow...
ignore:
- build-docker.yml
- publish-playground.yml
- publish-docs.yml

4
.gitignore vendored
View File

@@ -29,10 +29,6 @@ tracing.folded
tracing-flamechart.svg
tracing-flamegraph.svg
# insta
.rs.pending-snap
###
# Rust.gitignore
###

View File

@@ -4,7 +4,7 @@ exclude: |
(?x)^(
.github/workflows/release.yml|
crates/red_knot_vendored/vendor/.*|
crates/red_knot_project/resources/.*|
crates/red_knot_workspace/resources/.*|
crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*|
crates/ruff_notebook/resources/.*|
@@ -73,7 +73,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.2
rev: v0.9.1
hooks:
- id: ruff-format
- id: ruff
@@ -91,7 +91,7 @@ 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.2.2
rev: v1.0.1
hooks:
- id: zizmor
@@ -103,7 +103,7 @@ repos:
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.7
rev: v1.7.6
hooks:
- id: actionlint
stages:

View File

@@ -1,103 +1,5 @@
# Changelog
## 0.9.3
### Preview features
- \[`airflow`\] Argument `fail_stop` in DAG has been renamed as `fail_fast` (`AIR302`) ([#15633](https://github.com/astral-sh/ruff/pull/15633))
- \[`airflow`\] Extend `AIR303` with more symbols ([#15611](https://github.com/astral-sh/ruff/pull/15611))
- \[`flake8-bandit`\] Report all references to suspicious functions (`S3`) ([#15541](https://github.com/astral-sh/ruff/pull/15541))
- \[`flake8-pytest-style`\] Do not emit diagnostics for empty `for` loops (`PT012`, `PT031`) ([#15542](https://github.com/astral-sh/ruff/pull/15542))
- \[`flake8-simplify`\] Avoid double negations (`SIM103`) ([#15562](https://github.com/astral-sh/ruff/pull/15562))
- \[`pyflakes`\] Fix infinite loop with unused local import in `__init__.py` (`F401`) ([#15517](https://github.com/astral-sh/ruff/pull/15517))
- \[`pylint`\] Do not report methods with only one `EM101`-compatible `raise` (`PLR6301`) ([#15507](https://github.com/astral-sh/ruff/pull/15507))
- \[`pylint`\] Implement `redefined-slots-in-subclass` (`W0244`) ([#9640](https://github.com/astral-sh/ruff/pull/9640))
- \[`pyupgrade`\] Add rules to use PEP 695 generics in classes and functions (`UP046`, `UP047`) ([#15565](https://github.com/astral-sh/ruff/pull/15565), [#15659](https://github.com/astral-sh/ruff/pull/15659))
- \[`refurb`\] Implement `for-loop-writes` (`FURB122`) ([#10630](https://github.com/astral-sh/ruff/pull/10630))
- \[`ruff`\] Implement `needless-else` clause (`RUF047`) ([#15051](https://github.com/astral-sh/ruff/pull/15051))
- \[`ruff`\] Implement `starmap-zip` (`RUF058`) ([#15483](https://github.com/astral-sh/ruff/pull/15483))
### Rule changes
- \[`flake8-bugbear`\] Do not raise error if keyword argument is present and target-python version is less or equals than 3.9 (`B903`) ([#15549](https://github.com/astral-sh/ruff/pull/15549))
- \[`flake8-comprehensions`\] strip parentheses around generators in `unnecessary-generator-set` (`C401`) ([#15553](https://github.com/astral-sh/ruff/pull/15553))
- \[`flake8-pytest-style`\] Rewrite references to `.exception` (`PT027`) ([#15680](https://github.com/astral-sh/ruff/pull/15680))
- \[`flake8-simplify`\] Mark fixes as unsafe (`SIM201`, `SIM202`) ([#15626](https://github.com/astral-sh/ruff/pull/15626))
- \[`flake8-type-checking`\] Fix some safe fixes being labeled unsafe (`TC006`,`TC008`) ([#15638](https://github.com/astral-sh/ruff/pull/15638))
- \[`isort`\] Omit trailing whitespace in `unsorted-imports` (`I001`) ([#15518](https://github.com/astral-sh/ruff/pull/15518))
- \[`pydoclint`\] Allow ignoring one line docstrings for `DOC` rules ([#13302](https://github.com/astral-sh/ruff/pull/13302))
- \[`pyflakes`\] Apply redefinition fixes by source code order (`F811`) ([#15575](https://github.com/astral-sh/ruff/pull/15575))
- \[`pyflakes`\] Avoid removing too many imports in `redefined-while-unused` (`F811`) ([#15585](https://github.com/astral-sh/ruff/pull/15585))
- \[`pyflakes`\] Group redefinition fixes by source statement (`F811`) ([#15574](https://github.com/astral-sh/ruff/pull/15574))
- \[`pylint`\] Include name of base class in message for `redefined-slots-in-subclass` (`W0244`) ([#15559](https://github.com/astral-sh/ruff/pull/15559))
- \[`ruff`\] Update fix for `RUF055` to use `var == value` ([#15605](https://github.com/astral-sh/ruff/pull/15605))
### Formatter
- Fix bracket spacing for single-element tuples in f-string expressions ([#15537](https://github.com/astral-sh/ruff/pull/15537))
- Fix unstable f-string formatting for expressions containing a trailing comma ([#15545](https://github.com/astral-sh/ruff/pull/15545))
### Performance
- Avoid quadratic membership check in import fixes ([#15576](https://github.com/astral-sh/ruff/pull/15576))
### Server
- Allow `unsafe-fixes` settings for code actions ([#15666](https://github.com/astral-sh/ruff/pull/15666))
### Bug fixes
- \[`flake8-bandit`\] Add missing single-line/dotall regex flag (`S608`) ([#15654](https://github.com/astral-sh/ruff/pull/15654))
- \[`flake8-import-conventions`\] Fix infinite loop between `ICN001` and `I002` (`ICN001`) ([#15480](https://github.com/astral-sh/ruff/pull/15480))
- \[`flake8-simplify`\] Do not emit diagnostics for expressions inside string type annotations (`SIM222`, `SIM223`) ([#15405](https://github.com/astral-sh/ruff/pull/15405))
- \[`pyflakes`\] Treat arguments passed to the `default=` parameter of `TypeVar` as type expressions (`F821`) ([#15679](https://github.com/astral-sh/ruff/pull/15679))
- \[`pyupgrade`\] Avoid syntax error when the iterable is a non-parenthesized tuple (`UP028`) ([#15543](https://github.com/astral-sh/ruff/pull/15543))
- \[`ruff`\] Exempt `NewType` calls where the original type is immutable (`RUF009`) ([#15588](https://github.com/astral-sh/ruff/pull/15588))
- Preserve raw string prefix and escapes in all codegen fixes ([#15694](https://github.com/astral-sh/ruff/pull/15694))
### Documentation
- Generate documentation redirects for lowercase rule codes ([#15564](https://github.com/astral-sh/ruff/pull/15564))
- `TRY300`: Add some extra notes on not catching exceptions you didn't expect ([#15036](https://github.com/astral-sh/ruff/pull/15036))
## 0.9.2
### Preview features
- \[`airflow`\] Fix typo "security_managr" to "security_manager" (`AIR303`) ([#15463](https://github.com/astral-sh/ruff/pull/15463))
- \[`airflow`\] extend and fix AIR302 rules ([#15525](https://github.com/astral-sh/ruff/pull/15525))
- \[`fastapi`\] Handle parameters with `Depends` correctly (`FAST003`) ([#15364](https://github.com/astral-sh/ruff/pull/15364))
- \[`flake8-pytest-style`\] Implement pytest.warns diagnostics (`PT029`, `PT030`, `PT031`) ([#15444](https://github.com/astral-sh/ruff/pull/15444))
- \[`flake8-pytest-style`\] Test function parameters with default arguments (`PT028`) ([#15449](https://github.com/astral-sh/ruff/pull/15449))
- \[`flake8-type-checking`\] Avoid false positives for `|` in `TC008` ([#15201](https://github.com/astral-sh/ruff/pull/15201))
### Rule changes
- \[`flake8-todos`\] Allow VSCode GitHub PR extension style links in `missing-todo-link` (`TD003`) ([#15519](https://github.com/astral-sh/ruff/pull/15519))
- \[`pyflakes`\] Show syntax error message for `F722` ([#15523](https://github.com/astral-sh/ruff/pull/15523))
### Formatter
- Fix curly bracket spacing around f-string expressions containing curly braces ([#15471](https://github.com/astral-sh/ruff/pull/15471))
- Fix joining of f-strings with different quotes when using quote style `Preserve` ([#15524](https://github.com/astral-sh/ruff/pull/15524))
### Server
- Avoid indexing the same workspace multiple times ([#15495](https://github.com/astral-sh/ruff/pull/15495))
- Display context for `ruff.configuration` errors ([#15452](https://github.com/astral-sh/ruff/pull/15452))
### Configuration
- Remove `flatten` to improve deserialization error messages ([#15414](https://github.com/astral-sh/ruff/pull/15414))
### Bug fixes
- Parse triple-quoted string annotations as if parenthesized ([#15387](https://github.com/astral-sh/ruff/pull/15387))
- \[`fastapi`\] Update `Annotated` fixes (`FAST002`) ([#15462](https://github.com/astral-sh/ruff/pull/15462))
- \[`flake8-bandit`\] Check for `builtins` instead of `builtin` (`S102`, `PTH123`) ([#15443](https://github.com/astral-sh/ruff/pull/15443))
- \[`flake8-pathlib`\] Fix `--select` for `os-path-dirname` (`PTH120`) ([#15446](https://github.com/astral-sh/ruff/pull/15446))
- \[`ruff`\] Fix false positive on global keyword (`RUF052`) ([#15235](https://github.com/astral-sh/ruff/pull/15235))
## 0.9.1
### Preview features

164
Cargo.lock generated
View File

@@ -212,9 +212,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.8.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
[[package]]
name = "block-buffer"
@@ -477,7 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1062,7 +1062,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"ignore",
"walkdir",
]
@@ -1343,9 +1343,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.7.1"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -1374,11 +1374,11 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "inotify"
version = "0.11.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
dependencies = [
"bitflags 2.8.0",
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
@@ -1422,6 +1422,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -1601,7 +1610,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"libc",
"redox_syscall 0.5.3",
]
@@ -1648,9 +1657,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.25"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lsp-server"
@@ -1772,7 +1781,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1796,11 +1805,11 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
version = "8.0.0"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"filetime",
"fsevent-sys",
"inotify",
@@ -1810,14 +1819,17 @@ dependencies = [
"mio",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
name = "notify-types"
version = "2.0.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df"
dependencies = [
"instant",
]
[[package]]
name = "nu-ansi-term"
@@ -1883,9 +1895,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.5"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c55fdf45a2b1e929e3656d404395767e05c98b6ebd8157eb31e370077d545160"
checksum = "f80a48eb68b6a7da9829b8b0429011708f775af80676a91063d023a66a656106"
dependencies = [
"indexmap",
]
@@ -2324,56 +2336,25 @@ dependencies = [
"crossbeam",
"ctrlc",
"filetime",
"insta",
"insta-cmd",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
"red_knot_server",
"regex",
"red_knot_workspace",
"ruff_db",
"ruff_python_trivia",
"salsa",
"tempfile",
"toml",
"tracing",
"tracing-flame",
"tracing-subscriber",
"tracing-tree",
]
[[package]]
name = "red_knot_project"
version = "0.0.0"
dependencies = [
"anyhow",
"crossbeam",
"glob",
"insta",
"notify",
"pep440_rs",
"rayon",
"red_knot_python_semantic",
"red_knot_vendored",
"ruff_cache",
"ruff_db",
"ruff_macros",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash 2.1.0",
"salsa",
"serde",
"thiserror 2.0.11",
"toml",
"tracing",
]
[[package]]
name = "red_knot_python_semantic"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.8.0",
"bitflags 2.7.0",
"camino",
"compact_str",
"countme",
@@ -2420,7 +2401,7 @@ dependencies = [
"libc",
"lsp-server",
"lsp-types",
"red_knot_project",
"red_knot_workspace",
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
@@ -2475,14 +2456,39 @@ dependencies = [
"console_log",
"js-sys",
"log",
"red_knot_project",
"red_knot_python_semantic",
"red_knot_workspace",
"ruff_db",
"ruff_notebook",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "red_knot_workspace"
version = "0.0.0"
dependencies = [
"anyhow",
"crossbeam",
"glob",
"insta",
"notify",
"pep440_rs",
"rayon",
"red_knot_python_semantic",
"red_knot_vendored",
"ruff_cache",
"ruff_db",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash 2.1.0",
"salsa",
"serde",
"thiserror 2.0.11",
"toml",
"tracing",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -2498,7 +2504,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
]
[[package]]
@@ -2584,13 +2590,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.3"
version = "0.9.1"
dependencies = [
"anyhow",
"argfile",
"assert_fs",
"bincode",
"bitflags 2.8.0",
"bitflags 2.7.0",
"cachedir",
"chrono",
"clap",
@@ -2662,8 +2668,8 @@ dependencies = [
"criterion",
"mimalloc",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
"red_knot_workspace",
"ruff_db",
"ruff_linter",
"ruff_python_ast",
@@ -2818,11 +2824,11 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.9.3"
version = "0.9.1"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.8.0",
"bitflags 2.7.0",
"chrono",
"clap",
"colored 3.0.0",
@@ -2912,7 +2918,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.8.0",
"bitflags 2.7.0",
"compact_str",
"is-macro",
"itertools 0.14.0",
@@ -2994,7 +3000,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
@@ -3005,7 +3011,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.8.0",
"bitflags 2.7.0",
"bstr",
"compact_str",
"insta",
@@ -3037,7 +3043,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"is-macro",
"ruff_cache",
"ruff_index",
@@ -3056,7 +3062,7 @@ dependencies = [
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"unicode-ident",
]
@@ -3135,7 +3141,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.3"
version = "0.9.1"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3223,7 +3229,7 @@ version = "0.38.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
dependencies = [
"bitflags 2.8.0",
"bitflags 2.7.0",
"errno",
"libc",
"linux-raw-sys",
@@ -3406,9 +3412,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [
"itoa",
"memchr",
@@ -3505,9 +3511,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.7.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
[[package]]
name = "siphasher"
@@ -3927,9 +3933,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.9"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1"
checksum = "74ba258e9de86447f75edf6455fded8e5242704c6fccffe7bf8d7fb6daef1180"
dependencies = [
"indicatif",
"tracing",
@@ -4170,9 +4176,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.12.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
dependencies = [
"getrandom",
"rand",
@@ -4182,9 +4188,9 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.12.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144b419c512fdd5eaa4c2998813e32aaab2b257746ee038de93985a99635501d"
checksum = "c91084647266237a48351d05d55dee65bba9e1b597f555fcf54680f820284a1c"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -41,7 +41,7 @@ ruff_workspace = { path = "crates/ruff_workspace" }
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
red_knot_server = { path = "crates/red_knot_server" }
red_knot_test = { path = "crates/red_knot_test" }
red_knot_project = { path = "crates/red_knot_project", default-features = false }
red_knot_workspace = { path = "crates/red_knot_workspace", default-features = false }
aho-corasick = { version = "1.1.3" }
anstream = { version = "0.6.18" }
@@ -105,7 +105,7 @@ matchit = { version = "0.8.1" }
memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "8.0.0" }
notify = { version = "7.0.0" }
ordermap = { version = "0.5.0" }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }

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.9.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.3/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.1/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.9.3
rev: v0.9.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -13,7 +13,7 @@ license.workspace = true
[dependencies]
red_knot_python_semantic = { workspace = true }
red_knot_project = { workspace = true, features = ["zstd"] }
red_knot_workspace = { workspace = true, features = ["zstd"] }
red_knot_server = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache"] }
@@ -32,15 +32,9 @@ tracing-flame = { workspace = true }
tracing-tree = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_trivia = { workspace = true }
insta = { workspace = true, features = ["filters"] }
insta-cmd = { workspace = true }
filetime = { workspace = true }
regex = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true }
ruff_db = { workspace = true, features = ["testing"] }
[lints]
workspace = true

View File

@@ -6,12 +6,13 @@ use clap::Parser;
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use python_version::PythonVersion;
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_project::watch;
use red_knot_project::watch::ProjectWatcher;
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_python_semantic::SitePackages;
use red_knot_server::run_server;
use red_knot_workspace::db::ProjectDatabase;
use red_knot_workspace::project::settings::Configuration;
use red_knot_workspace::project::ProjectMetadata;
use red_knot_workspace::watch;
use red_knot_workspace::watch::ProjectWatcher;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
@@ -70,24 +71,31 @@ struct Args {
}
impl Args {
fn to_options(&self) -> Options {
Options {
environment: Some(EnvironmentOptions {
python_version: self
.python_version
.map(|version| RangedValue::cli(version.into())),
venv_path: self.venv_path.as_ref().map(RelativePathBuf::cli),
typeshed: self.typeshed.as_ref().map(RelativePathBuf::cli),
extra_paths: self.extra_search_path.as_ref().map(|extra_search_paths| {
extra_search_paths
.iter()
.map(RelativePathBuf::cli)
.collect()
}),
..EnvironmentOptions::default()
}),
..Default::default()
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
let mut configuration = Configuration::default();
if let Some(python_version) = self.python_version {
configuration.python_version = Some(python_version.into());
}
if let Some(venv_path) = &self.venv_path {
configuration.search_paths.site_packages = Some(SitePackages::Derived {
venv_path: SystemPath::absolute(venv_path, cli_cwd),
});
}
if let Some(typeshed) = &self.typeshed {
configuration.search_paths.typeshed = Some(SystemPath::absolute(typeshed, cli_cwd));
}
if let Some(extra_search_paths) = &self.extra_search_path {
configuration.search_paths.extra_paths = extra_search_paths
.iter()
.map(|path| Some(SystemPath::absolute(path, cli_cwd)))
.collect();
}
configuration
}
}
@@ -155,14 +163,19 @@ fn run() -> anyhow::Result<ExitStatus> {
.transpose()?
.unwrap_or_else(|| cli_base_path.clone());
let system = OsSystem::new(cwd);
let cli_options = args.to_options();
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
workspace_metadata.apply_cli_options(cli_options.clone());
let system = OsSystem::new(cwd.clone());
let cli_configuration = args.to_configuration(&cwd);
let workspace_metadata = ProjectMetadata::discover(
system.current_directory(),
&system,
Some(&cli_configuration),
)?;
// TODO: Use the `program_settings` to compute the key for the database's persistent
// cache and load the cache if it exists.
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_configuration);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -215,11 +228,11 @@ struct MainLoop {
/// The file system watcher, if running in watch mode.
watcher: Option<ProjectWatcher>,
cli_options: Options,
cli_configuration: Configuration,
}
impl MainLoop {
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
fn new(cli_configuration: Configuration) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
(
@@ -227,7 +240,7 @@ impl MainLoop {
sender: sender.clone(),
receiver,
watcher: None,
cli_options,
cli_configuration,
},
MainLoopCancellationToken { sender },
)
@@ -311,7 +324,7 @@ impl MainLoop {
MainLoopMessage::ApplyChanges(changes) => {
revision += 1;
// Automatically cancels any pending queries and waits for them to complete.
db.apply_changes(changes, Some(&self.cli_options));
db.apply_changes(changes, Some(&self.cli_configuration));
if let Some(watcher) = self.watcher.as_mut() {
watcher.update(db);
}

View File

@@ -1,327 +0,0 @@
use anyhow::Context;
use insta::Settings;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
/// Specifying an option on the CLI should take precedence over the same setting in the
/// project's configuration.
#[test]
fn config_override() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.environment]
python-version = "3.11"
"#,
),
(
"test.py",
r#"
import sys
# Access `sys.last_exc` that was only added in Python 3.12
print(sys.last_exc)
"#,
),
])?;
case.insta_settings().bind(|| {
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
----- stderr -----
");
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
});
Ok(())
}
/// Paths specified on the CLI are relative to the current working directory and not the project root.
///
/// We test this by adding an extra search path from the CLI to the libs directory when
/// running the CLI from the child directory (using relative paths).
///
/// Project layout:
/// ```
/// - libs
/// |- utils.py
/// - child
/// | - test.py
/// - pyproject.toml
/// ```
///
/// And the command is run in the `child` directory.
#[test]
fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.environment]
python-version = "3.11"
"#,
),
(
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
a + b
"#,
),
(
"child/test.py",
r#"
from utils import add
stat = add(10, 15)
"#,
),
])?;
case.insta_settings().bind(|| {
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
success: false
exit_code: 1
----- stdout -----
error[lint:unresolved-import] <temp_dir>/child/test.py:2:1 Cannot resolve import `utils`
----- stderr -----
"#);
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
});
Ok(())
}
/// Paths specified in a configuration file are relative to the project root.
///
/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run
/// the CLI from a subdirectory.
///
/// Project layout:
/// ```
/// - libs
/// |- utils.py
/// - child
/// | - test.py
/// - pyproject.toml
/// ```
#[test]
fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.environment]
python-version = "3.11"
extra-paths = ["libs"]
"#,
),
(
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
a + b
"#,
),
(
"child/test.py",
r#"
from utils import add
stat = add(10, 15)
"#,
),
])?;
case.insta_settings().bind(|| {
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
});
Ok(())
}
/// The rule severity can be changed in the configuration file
#[test]
fn rule_severity() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
y = 4 / 0
for a in range(0, y):
x = a
print(x) # possibly-unresolved-reference
"#,
)?;
case.insta_settings().bind(|| {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
----- stderr -----
");
case.write_file("pyproject.toml", r#"
[tool.knot.rules]
division-by-zero = "warn" # demote to warn
possibly-unresolved-reference = "ignore"
"#)?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
----- stderr -----
");
Ok(())
})
}
/// Red Knot warns about unknown rules
#[test]
fn unknown_rules() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.rules]
division-by-zer = "warn" # incorrect rule name
"#,
),
("test.py", "print(10)"),
])?;
case.insta_settings().bind(|| {
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
----- stderr -----
");
});
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
project_dir: PathBuf,
}
impl TestCase {
fn new() -> anyhow::Result<Self> {
let temp_dir = TempDir::new()?;
// Canonicalize the tempdir path because macos uses symlinks for tempdirs
// and that doesn't play well with our snapshot filtering.
let project_dir = temp_dir
.path()
.canonicalize()
.context("Failed to canonicalize project path")?;
Ok(Self {
project_dir,
_temp_dir: temp_dir,
})
}
fn with_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_files(files)?;
Ok(case)
}
fn with_file(path: impl AsRef<Path>, content: &str) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_file(path, content)?;
Ok(case)
}
fn write_files<'a>(
&self,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<()> {
for (path, content) in files {
self.write_file(path, content)?;
}
Ok(())
}
fn write_file(&self, path: impl AsRef<Path>, content: &str) -> anyhow::Result<()> {
let path = path.as_ref();
let path = self.project_dir.join(path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
}
std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content))
.with_context(|| format!("Failed to write file `{path}`", path = path.display()))?;
Ok(())
}
fn project_dir(&self) -> &Path {
&self.project_dir
}
// Returns the insta filters to escape paths in snapshots
fn insta_settings(&self) -> Settings {
let mut settings = insta::Settings::clone_current();
settings.add_filter(&tempdir_filter(&self.project_dir), "<temp_dir>/");
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
settings
}
fn command(&self) -> Command {
let mut command = Command::new(get_cargo_bin("red_knot"));
command.current_dir(&self.project_dir);
command
}
}
fn tempdir_filter(path: &Path) -> String {
format!(r"{}\\?/?", regex::escape(path.to_str().unwrap()))
}

View File

@@ -4,12 +4,11 @@ use std::io::Write;
use std::time::{Duration, Instant};
use anyhow::{anyhow, Context};
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::pyproject::{PyProject, Tool};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
use red_knot_workspace::db::{Db, ProjectDatabase};
use red_knot_workspace::project::settings::{Configuration, SearchPathConfiguration};
use red_knot_workspace::project::ProjectMetadata;
use red_knot_workspace::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
use ruff_db::files::{system_path_to_file, File, FileError};
use ruff_db::source::source_text;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -23,6 +22,7 @@ struct TestCase {
/// We need to hold on to it in the test case or the temp files get deleted.
_temp_dir: tempfile::TempDir,
root_dir: SystemPathBuf,
configuration: Configuration,
}
impl TestCase {
@@ -112,44 +112,19 @@ impl TestCase {
Ok(all_events)
}
fn take_watch_changes<M: MatchEvent>(&self, matcher: M) -> Vec<ChangeEvent> {
self.try_take_watch_changes(matcher, Duration::from_secs(10))
fn take_watch_changes(&self) -> Vec<ChangeEvent> {
self.try_take_watch_changes(Duration::from_secs(10))
.expect("Expected watch changes but observed none")
}
fn try_take_watch_changes<M: MatchEvent>(
&self,
mut matcher: M,
timeout: Duration,
) -> Result<Vec<ChangeEvent>, Vec<ChangeEvent>> {
let watcher = self
.watcher
.as_ref()
.expect("Cannot call `try_take_watch_changes` after `stop_watch`");
fn try_take_watch_changes(&self, timeout: Duration) -> Option<Vec<ChangeEvent>> {
let watcher = self.watcher.as_ref()?;
let start = Instant::now();
let mut all_events = Vec::new();
loop {
let events = self
.changes_receiver
.recv_timeout(Duration::from_millis(100))
.unwrap_or_default();
if events
.iter()
.any(|event| matcher.match_event(event) || event.is_rescan())
{
all_events.extend(events);
break;
}
all_events.extend(events);
if start.elapsed() > timeout {
return Err(all_events);
}
}
let mut all_events = self
.changes_receiver
.recv_timeout(timeout)
.unwrap_or_default();
watcher.flush();
while let Ok(event) = self
.changes_receiver
@@ -159,28 +134,26 @@ impl TestCase {
watcher.flush();
}
Ok(all_events)
if all_events.is_empty() {
return None;
}
Some(all_events)
}
fn apply_changes(&mut self, changes: Vec<ChangeEvent>) {
self.db.apply_changes(changes, None);
self.db.apply_changes(changes, Some(&self.configuration));
}
fn update_options(&mut self, options: Options) -> anyhow::Result<()> {
std::fs::write(
self.project_path("pyproject.toml").as_std_path(),
toml::to_string(&PyProject {
project: None,
tool: Some(Tool {
knot: Some(options),
}),
})
.context("Failed to serialize options")?,
)
.context("Failed to write configuration")?;
fn update_search_path_settings(
&mut self,
configuration: SearchPathConfiguration,
) -> anyhow::Result<()> {
let program = Program::get(self.db());
let changes = self.take_watch_changes(event_for_file("pyproject.toml"));
self.apply_changes(changes);
let new_settings = configuration.to_settings(self.db.project().root(&self.db));
self.configuration.search_paths = configuration;
program.update_search_paths(&mut self.db, &new_settings)?;
if let Some(watcher) = &mut self.watcher {
watcher.update(&self.db);
@@ -261,13 +234,14 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
where
F: SetupFiles,
{
setup_with_options(setup_files, |_root, _project_path| None)
setup_with_search_paths(setup_files, |_root, _project_path| {
SearchPathConfiguration::default()
})
}
// TODO: Replace with configuration?
fn setup_with_options<F>(
fn setup_with_search_paths<F>(
setup_files: F,
create_options: impl FnOnce(&SystemPath, &SystemPath) -> Option<Options>,
create_search_paths: impl FnOnce(&SystemPath, &SystemPath) -> SearchPathConfiguration,
) -> anyhow::Result<TestCase>
where
F: SetupFiles,
@@ -301,33 +275,32 @@ where
let system = OsSystem::new(&project_path);
if let Some(options) = create_options(&root_path, &project_path) {
std::fs::write(
project_path.join("pyproject.toml").as_std_path(),
toml::to_string(&PyProject {
project: None,
tool: Some(Tool {
knot: Some(options),
}),
})
.context("Failed to serialize options")?,
)
.context("Failed to write configuration")?;
}
let search_paths = create_search_paths(&root_path, &project_path);
let project = ProjectMetadata::discover(&project_path, &system)?;
let program_settings = project.to_program_settings(&system);
for path in program_settings
.search_paths
for path in search_paths
.extra_paths
.iter()
.chain(program_settings.search_paths.custom_typeshed.as_ref())
.flatten()
.chain(search_paths.typeshed.iter())
.chain(search_paths.site_packages.iter().flat_map(|site_packages| {
if let SitePackages::Known(path) = site_packages {
path.as_slice()
} else {
&[]
}
}))
{
std::fs::create_dir_all(path.as_std_path())
.with_context(|| format!("Failed to create search path `{path}`"))?;
}
let configuration = Configuration {
python_version: Some(PythonVersion::PY312),
search_paths,
};
let project = ProjectMetadata::discover(&project_path, &system, Some(&configuration))?;
let db = ProjectDatabase::new(project, system)?;
let (sender, receiver) = crossbeam::channel::unbounded();
@@ -343,12 +316,12 @@ where
watcher: Some(watcher),
_temp_dir: temp_dir,
root_dir: root_path,
configuration,
};
// Sometimes the file watcher reports changes for events that happened before the watcher was started.
// Do a best effort at dropping these events.
let _ =
test_case.try_take_watch_changes(|_event: &ChangeEvent| true, Duration::from_millis(100));
test_case.try_take_watch_changes(Duration::from_millis(100));
Ok(test_case)
}
@@ -789,15 +762,13 @@ fn directory_deleted() -> anyhow::Result<()> {
#[test]
fn search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
..EnvironmentOptions::default()
}),
..Options::default()
})
})?;
let mut case =
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, _project_path| {
SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
..SearchPathConfiguration::default()
}
})?;
let site_packages = case.root_path().join("site_packages");
@@ -831,12 +802,9 @@ fn add_search_path() -> anyhow::Result<()> {
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_none());
// Register site-packages as a search path.
case.update_options(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli("site_packages")]),
..EnvironmentOptions::default()
}),
..Options::default()
case.update_search_path_settings(SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![site_packages.clone()])),
..SearchPathConfiguration::default()
})
.expect("Search path settings to be valid");
@@ -853,22 +821,19 @@ fn add_search_path() -> anyhow::Result<()> {
#[test]
fn remove_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
..EnvironmentOptions::default()
}),
..Options::default()
})
})?;
let mut case =
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, _project_path| {
SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
..SearchPathConfiguration::default()
}
})?;
// Remove site packages from the search path settings.
let site_packages = case.root_path().join("site_packages");
case.update_options(Options {
environment: None,
..Options::default()
case.update_search_path_settings(SearchPathConfiguration {
site_packages: None,
..SearchPathConfiguration::default()
})
.expect("Search path settings to be valid");
@@ -881,67 +846,9 @@ fn remove_search_path() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn change_python_version_and_platform() -> anyhow::Result<()> {
let mut case = setup_with_options(
// `sys.last_exc` is a Python 3.12 only feature
// `os.getegid()` is Unix only
[(
"bar.py",
r#"
import sys
import os
print(sys.last_exc, os.getegid())
"#,
)],
|_root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
"win32".to_string(),
))),
..EnvironmentOptions::default()
}),
..Options::default()
})
},
)?;
let diagnostics = case.db.check().context("Failed to check project.")?;
assert_eq!(diagnostics.len(), 2);
assert_eq!(
diagnostics[0].message(),
"Type `<module 'sys'>` has no attribute `last_exc`"
);
assert_eq!(
diagnostics[1].message(),
"Type `<module 'os'>` has no attribute `getegid`"
);
// Change the python version
case.update_options(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
"linux".to_string(),
))),
..EnvironmentOptions::default()
}),
..Options::default()
})
.expect("Search path settings to be valid");
let diagnostics = case.db.check().context("Failed to check project.")?;
assert!(diagnostics.is_empty());
Ok(())
}
#[test]
fn changed_versions_file() -> anyhow::Result<()> {
let mut case = setup_with_options(
let mut case = setup_with_search_paths(
|root_path: &SystemPath, project_path: &SystemPath| {
std::fs::write(project_path.join("bar.py").as_std_path(), "import sub.a")?;
std::fs::create_dir_all(root_path.join("typeshed/stdlib").as_std_path())?;
@@ -953,14 +860,9 @@ fn changed_versions_file() -> anyhow::Result<()> {
Ok(())
},
|root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
typeshed: Some(RelativePathBuf::cli(root_path.join("typeshed"))),
..EnvironmentOptions::default()
}),
..Options::default()
})
|root_path, _project_path| SearchPathConfiguration {
typeshed: Some(root_path.join("typeshed")),
..SearchPathConfiguration::default()
},
)?;
@@ -1225,7 +1127,7 @@ mod unix {
update_file(baz_original, "def baz(): print('Version 2')")
.context("Failed to update bar/baz.py")?;
let changes = case.take_watch_changes(event_for_file("baz.py"));
let changes = case.take_watch_changes();
case.apply_changes(changes);
@@ -1357,7 +1259,7 @@ mod unix {
/// ```
#[test]
fn symlinked_module_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options(
let mut case = setup_with_search_paths(
|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let site_packages = root.join("site-packages");
@@ -1380,17 +1282,11 @@ mod unix {
Ok(())
},
|_root, _project| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(
".venv/lib/python3.12/site-packages",
)]),
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
..EnvironmentOptions::default()
}),
..Options::default()
})
|_root, project| SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![
project.join(".venv/lib/python3.12/site-packages")
])),
..SearchPathConfiguration::default()
},
)?;

View File

@@ -1,190 +0,0 @@
use std::{collections::HashMap, hash::BuildHasher};
use red_knot_python_semantic::{PythonPlatform, PythonVersion, SitePackages};
use ruff_db::system::SystemPathBuf;
/// Combine two values, preferring the values in `self`.
///
/// The logic should follow that of Cargo's `config.toml`:
///
/// > If a key is specified in multiple config files, the values will get merged together.
/// > Numbers, strings, and booleans will use the value in the deeper config directory taking
/// > precedence over ancestor directories, where the home directory is the lowest priority.
/// > Arrays will be joined together with higher precedence items being placed later in the
/// > merged array.
///
/// ## uv Compatibility
///
/// The merging behavior differs from uv in that values with higher precedence in arrays
/// are placed later in the merged array. This is because we want to support overriding
/// earlier values and values from other configurations, including unsetting them.
/// For example: patterns coming last in file inclusion and exclusion patterns
/// allow overriding earlier patterns, matching the `gitignore` behavior.
/// Generally speaking, it feels more intuitive if later values override earlier values
/// than the other way around: `knot --exclude png --exclude "!important.png"`.
///
/// The main downside of this approach is that the ordering can be surprising in cases
/// where the option has a "first match" semantic and not a "last match" wins.
/// One such example is `extra-paths` where the semantics is given by Python:
/// the module on the first matching search path wins.
///
/// ```toml
/// [environment]
/// extra-paths = ["b", "c"]
/// ```
///
/// ```bash
/// knot --extra-paths a
/// ```
///
/// That's why a user might expect that this configuration results in `["a", "b", "c"]`,
/// because the CLI has higher precedence. However, the current implementation results in a
/// resolved extra search path of `["b", "c", "a"]`, which means `a` will be tried last.
///
/// There's an argument here that the user should be able to specify the order of the paths,
/// because only then is the user in full control of where to insert the path when specyifing `extra-paths`
/// in multiple sources.
///
/// ## Macro
/// You can automatically derive `Combine` for structs with named fields by using `derive(ruff_macros::Combine)`.
pub trait Combine {
#[must_use]
fn combine(mut self, other: Self) -> Self
where
Self: Sized,
{
self.combine_with(other);
self
}
fn combine_with(&mut self, other: Self);
}
impl<T> Combine for Option<T>
where
T: Combine,
{
fn combine(self, other: Self) -> Self
where
Self: Sized,
{
match (self, other) {
(Some(a), Some(b)) => Some(a.combine(b)),
(None, Some(b)) => Some(b),
(a, _) => a,
}
}
fn combine_with(&mut self, other: Self) {
match (self, other) {
(Some(a), Some(b)) => {
a.combine_with(b);
}
(a @ None, Some(b)) => {
*a = Some(b);
}
_ => {}
}
}
}
impl<T> Combine for Vec<T> {
fn combine_with(&mut self, mut other: Self) {
// `self` takes precedence over `other` but values with higher precedence must be placed after.
// Swap the vectors so that `other` is the one that gets extended, so that the values of `self` come after.
std::mem::swap(self, &mut other);
self.extend(other);
}
}
impl<K, V, S> Combine for HashMap<K, V, S>
where
K: Eq + std::hash::Hash,
S: BuildHasher,
{
fn combine_with(&mut self, mut other: Self) {
// `self` takes precedence over `other` but `extend` overrides existing values.
// Swap the hash maps so that `self` is the one that gets extended.
std::mem::swap(self, &mut other);
self.extend(other);
}
}
/// Implements [`Combine`] for a value that always returns `self` when combined with another value.
macro_rules! impl_noop_combine {
($name:ident) => {
impl Combine for $name {
#[inline(always)]
fn combine_with(&mut self, _other: Self) {}
#[inline(always)]
fn combine(self, _other: Self) -> Self {
self
}
}
};
}
impl_noop_combine!(SystemPathBuf);
impl_noop_combine!(PythonPlatform);
impl_noop_combine!(SitePackages);
impl_noop_combine!(PythonVersion);
// std types
impl_noop_combine!(bool);
impl_noop_combine!(usize);
impl_noop_combine!(u8);
impl_noop_combine!(u16);
impl_noop_combine!(u32);
impl_noop_combine!(u64);
impl_noop_combine!(u128);
impl_noop_combine!(isize);
impl_noop_combine!(i8);
impl_noop_combine!(i16);
impl_noop_combine!(i32);
impl_noop_combine!(i64);
impl_noop_combine!(i128);
impl_noop_combine!(String);
#[cfg(test)]
mod tests {
use crate::combine::Combine;
use std::collections::HashMap;
#[test]
fn combine_option() {
assert_eq!(Some(1).combine(Some(2)), Some(1));
assert_eq!(None.combine(Some(2)), Some(2));
assert_eq!(Some(1).combine(None), Some(1));
}
#[test]
fn combine_vec() {
assert_eq!(None.combine(Some(vec![1, 2, 3])), Some(vec![1, 2, 3]));
assert_eq!(Some(vec![1, 2, 3]).combine(None), Some(vec![1, 2, 3]));
assert_eq!(
Some(vec![1, 2, 3]).combine(Some(vec![4, 5, 6])),
Some(vec![4, 5, 6, 1, 2, 3])
);
}
#[test]
fn combine_map() {
let a: HashMap<u32, _> = HashMap::from_iter([(1, "a"), (2, "a"), (3, "a")]);
let b: HashMap<u32, _> = HashMap::from_iter([(0, "b"), (2, "b"), (5, "b")]);
assert_eq!(None.combine(Some(b.clone())), Some(b.clone()));
assert_eq!(Some(a.clone()).combine(None), Some(a.clone()));
assert_eq!(
Some(a).combine(Some(b)),
Some(HashMap::from_iter([
(0, "b"),
// The value from `a` takes precedence
(1, "a"),
(2, "a"),
(3, "a"),
(5, "b")
]))
);
}
}

View File

@@ -1,268 +0,0 @@
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
use crate::Db;
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use red_knot_python_semantic::{
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{System, SystemPath};
use ruff_macros::Combine;
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::Debug;
use thiserror::Error;
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Options {
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<EnvironmentOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<SrcOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
}
impl Options {
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, KnotTomlError> {
let _guard = ValueSourceGuard::new(source);
let options = toml::from_str(content)?;
Ok(options)
}
pub(crate) fn to_program_settings(
&self,
project_root: &SystemPath,
system: &dyn System,
) -> ProgramSettings {
let (python_version, python_platform) = self
.environment
.as_ref()
.map(|env| {
(
env.python_version.as_deref().copied(),
env.python_platform.as_deref(),
)
})
.unwrap_or_default();
ProgramSettings {
python_version: python_version.unwrap_or_default(),
python_platform: python_platform.cloned().unwrap_or_default(),
search_paths: self.to_search_path_settings(project_root, system),
}
}
fn to_search_path_settings(
&self,
project_root: &SystemPath,
system: &dyn System,
) -> SearchPathSettings {
let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref())
{
vec![src_root.absolute(project_root, system)]
} else {
let src = project_root.join("src");
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
if system.is_directory(&src) {
vec![project_root.to_path_buf(), src]
} else {
vec![project_root.to_path_buf()]
}
};
let (extra_paths, python, typeshed) = self
.environment
.as_ref()
.map(|env| {
(
env.extra_paths.clone(),
env.venv_path.clone(),
env.typeshed.clone(),
)
})
.unwrap_or_default();
SearchPathSettings {
extra_paths: extra_paths
.unwrap_or_default()
.into_iter()
.map(|path| path.absolute(project_root, system))
.collect(),
src_roots,
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
site_packages: python
.map(|venv_path| SitePackages::Derived {
venv_path: venv_path.absolute(project_root, system),
})
.unwrap_or(SitePackages::Known(vec![])),
}
}
#[must_use]
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
let registry = db.lint_registry();
let mut diagnostics = Vec::new();
// Initialize the selection with the defaults
let mut selection = RuleSelection::from_registry(registry);
let rules = self
.rules
.as_ref()
.into_iter()
.flat_map(|rules| rules.inner.iter());
for (rule_name, level) in rules {
let source = rule_name.source();
match registry.get(rule_name) {
Ok(lint) => {
let lint_source = match source {
ValueSource::File(_) => LintSource::File,
ValueSource::Cli => LintSource::Cli,
};
if let Ok(severity) = Severity::try_from(**level) {
selection.enable(lint, severity, lint_source);
} else {
selection.disable(lint);
}
}
Err(error) => {
// `system_path_to_file` can return `Err` if the file was deleted since the configuration
// was read. This should be rare and it should be okay to default to not showing a configuration
// file in that case.
let file = source
.file()
.and_then(|path| system_path_to_file(db.upcast(), path).ok());
// TODO: Add a note if the value was configured on the CLI
let diagnostic = match error {
GetLintError::Unknown(_) => OptionDiagnostic::new(
DiagnosticId::UnknownRule,
format!("Unknown lint rule `{rule_name}`"),
Severity::Warning,
),
GetLintError::Removed(_) => OptionDiagnostic::new(
DiagnosticId::UnknownRule,
format!("Unknown lint rule `{rule_name}`"),
Severity::Warning,
),
};
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
}
}
}
(selection, diagnostics)
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct EnvironmentOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub python_version: Option<RangedValue<PythonVersion>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub python_platform: Option<RangedValue<PythonPlatform>>,
/// List of user-provided paths that should take first priority in the module resolution.
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
/// or pyright's stubPath configuration setting.
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_paths: Option<Vec<RelativePathBuf>>,
/// Optional path to a "typeshed" directory on disk for us to use for standard-library types.
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
/// bundled as a zip file in the binary
#[serde(skip_serializing_if = "Option::is_none")]
pub typeshed: Option<RelativePathBuf>,
// TODO: Rename to python, see https://github.com/astral-sh/ruff/issues/15530
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
#[serde(skip_serializing_if = "Option::is_none")]
pub venv_path: Option<RelativePathBuf>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SrcOptions {
/// The root of the project, used for finding first-party modules.
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<RelativePathBuf>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", transparent)]
pub struct Rules {
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
}
#[derive(Error, Debug)]
pub enum KnotTomlError {
#[error(transparent)]
TomlSyntax(#[from] toml::de::Error),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OptionDiagnostic {
id: DiagnosticId,
message: String,
severity: Severity,
file: Option<File>,
range: Option<TextRange>,
}
impl OptionDiagnostic {
pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self {
Self {
id,
message,
severity,
file: None,
range: None,
}
}
#[must_use]
fn with_file(mut self, file: Option<File>) -> Self {
self.file = file;
self
}
#[must_use]
fn with_range(mut self, range: Option<TextRange>) -> Self {
self.range = range;
self
}
}
impl Diagnostic for OptionDiagnostic {
fn id(&self) -> DiagnosticId {
self.id
}
fn message(&self) -> Cow<str> {
Cow::Borrowed(&self.message)
}
fn file(&self) -> Option<File> {
self.file
}
fn range(&self) -> Option<TextRange> {
self.range
}
fn severity(&self) -> Severity {
self.severity
}
}

View File

@@ -1,337 +0,0 @@
use crate::combine::Combine;
use crate::Db;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use toml::Spanned;
#[derive(Clone, Debug)]
pub enum ValueSource {
/// Value loaded from a project's configuration file.
///
/// Ideally, we'd use [`ruff_db::files::File`] but we can't because the database hasn't been
/// created when loading the configuration.
File(Arc<SystemPathBuf>),
/// The value comes from a CLI argument, while it's left open if specified using a short argument,
/// long argument (`--extra-paths`) or `--config key=value`.
Cli,
}
impl ValueSource {
pub fn file(&self) -> Option<&SystemPath> {
match self {
ValueSource::File(path) => Some(&**path),
ValueSource::Cli => None,
}
}
}
thread_local! {
/// Serde doesn't provide any easy means to pass a value to a [`Deserialize`] implementation,
/// but we want to associate each deserialized [`RelativePath`] with the source from
/// which it originated. We use a thread local variable to work around this limitation.
///
/// Use the [`ValueSourceGuard`] to initialize the thread local before calling into any
/// deserialization code. It ensures that the thread local variable gets cleaned up
/// once deserialization is done (once the guard gets dropped).
static VALUE_SOURCE: RefCell<Option<ValueSource>> = const { RefCell::new(None) };
}
/// Guard to safely change the [`VALUE_SOURCE`] for the current thread.
#[must_use]
pub(super) struct ValueSourceGuard {
prev_value: Option<ValueSource>,
}
impl ValueSourceGuard {
pub(super) fn new(source: ValueSource) -> Self {
let prev = VALUE_SOURCE.replace(Some(source));
Self { prev_value: prev }
}
}
impl Drop for ValueSourceGuard {
fn drop(&mut self) {
VALUE_SOURCE.set(self.prev_value.take());
}
}
/// A value that "remembers" where it comes from (source) and its range in source.
///
/// ## Equality, Hash, and Ordering
/// The equality, hash, and ordering are solely based on the value. They disregard the value's range
/// or source.
///
/// This ensures that two resolved configurations are identical even if the position of a value has changed
/// or if the values were loaded from different sources.
#[derive(Clone)]
pub struct RangedValue<T> {
value: T,
source: ValueSource,
/// The byte range of `value` in `source`.
///
/// Can be `None` because not all sources support a range.
/// For example, arguments provided on the CLI won't have a range attached.
range: Option<TextRange>,
}
impl<T> RangedValue<T> {
pub fn new(value: T, source: ValueSource) -> Self {
Self::with_range(value, source, TextRange::default())
}
pub fn cli(value: T) -> Self {
Self::with_range(value, ValueSource::Cli, TextRange::default())
}
pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self {
Self {
value,
range: Some(range),
source,
}
}
pub fn range(&self) -> Option<TextRange> {
self.range
}
pub fn source(&self) -> &ValueSource {
&self.source
}
#[must_use]
pub fn with_source(mut self, source: ValueSource) -> Self {
self.source = source;
self
}
pub fn into_inner(self) -> T {
self.value
}
}
impl<T> Combine for RangedValue<T> {
fn combine(self, _other: Self) -> Self
where
Self: Sized,
{
self
}
fn combine_with(&mut self, _other: Self) {}
}
impl<T> IntoIterator for RangedValue<T>
where
T: IntoIterator,
{
type Item = T::Item;
type IntoIter = T::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.value.into_iter()
}
}
// The type already has an `iter` method thanks to `Deref`.
#[allow(clippy::into_iter_without_iter)]
impl<'a, T> IntoIterator for &'a RangedValue<T>
where
&'a T: IntoIterator,
{
type Item = <&'a T as IntoIterator>::Item;
type IntoIter = <&'a T as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.value.into_iter()
}
}
// The type already has a `into_iter_mut` method thanks to `DerefMut`.
#[allow(clippy::into_iter_without_iter)]
impl<'a, T> IntoIterator for &'a mut RangedValue<T>
where
&'a mut T: IntoIterator,
{
type Item = <&'a mut T as IntoIterator>::Item;
type IntoIter = <&'a mut T as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.value.into_iter()
}
}
impl<T> fmt::Debug for RangedValue<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.value.fmt(f)
}
}
impl<T> fmt::Display for RangedValue<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.value.fmt(f)
}
}
impl<T> Deref for RangedValue<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> DerefMut for RangedValue<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.value
}
}
impl<T, U: ?Sized> AsRef<U> for RangedValue<T>
where
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
self.value.as_ref()
}
}
impl<T: PartialEq> PartialEq for RangedValue<T> {
fn eq(&self, other: &Self) -> bool {
self.value.eq(&other.value)
}
}
impl<T: PartialEq<T>> PartialEq<T> for RangedValue<T> {
fn eq(&self, other: &T) -> bool {
self.value.eq(other)
}
}
impl<T: Eq> Eq for RangedValue<T> {}
impl<T: Hash> Hash for RangedValue<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
impl<T: PartialOrd> PartialOrd for RangedValue<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value.partial_cmp(&other.value)
}
}
impl<T: PartialOrd<T>> PartialOrd<T> for RangedValue<T> {
fn partial_cmp(&self, other: &T) -> Option<Ordering> {
self.value.partial_cmp(other)
}
}
impl<T: Ord> Ord for RangedValue<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
}
}
impl<'de, T> Deserialize<'de> for RangedValue<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
let span = spanned.span();
let range = TextRange::new(
TextSize::try_from(span.start).expect("Configuration file to be smaller than 4GB"),
TextSize::try_from(span.end).expect("Configuration file to be smaller than 4GB"),
);
Ok(VALUE_SOURCE.with_borrow(|source| {
let source = source.clone().unwrap();
Self::with_range(spanned.into_inner(), source, range)
}))
}
}
impl<T> Serialize for RangedValue<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.value.serialize(serializer)
}
}
/// A possibly relative path in a configuration file.
///
/// Relative paths in configuration files or from CLI options
/// require different anchoring:
///
/// * CLI: The path is relative to the current working directory
/// * Configuration file: The path is relative to the project's root.
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
#[serde(transparent)]
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);
impl RelativePathBuf {
pub fn new(path: impl AsRef<SystemPath>, source: ValueSource) -> Self {
Self(RangedValue::new(path.as_ref().to_path_buf(), source))
}
pub fn cli(path: impl AsRef<SystemPath>) -> Self {
Self::new(path, ValueSource::Cli)
}
/// Returns the relative path as specified by the user.
pub fn path(&self) -> &SystemPath {
&self.0
}
/// Returns the owned relative path.
pub fn into_path_buf(self) -> SystemPathBuf {
self.0.into_inner()
}
/// Resolves the absolute path for `self` based on its origin.
pub fn absolute_with_db(&self, db: &dyn Db) -> SystemPathBuf {
self.absolute(db.project().root(db), db.system())
}
/// Resolves the absolute path for `self` based on its origin.
pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf {
let relative_to = match &self.0.source {
ValueSource::File(_) => project_root,
ValueSource::Cli => system.current_directory(),
};
SystemPath::absolute(&self.0, relative_to)
}
}
impl Combine for RelativePathBuf {
fn combine(self, other: Self) -> Self {
Self(self.0.combine(other.0))
}
fn combine_with(&mut self, other: Self) {
self.0.combine_with(other.0);
}
}

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: sub_project
---
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.10"),
)),
),
)

View File

@@ -1,9 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: sub_project
---
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(),
)

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("super-app"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -1,9 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: project
---
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)

View File

@@ -1,9 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: project
---
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)

View File

@@ -1,220 +0,0 @@
"""A runner for Markdown-based tests for Red Knot"""
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "rich",
# "watchfiles",
# ]
# ///
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
from typing import Final, Literal, Never, assert_never
from rich.console import Console
from watchfiles import Change, watch
CRATE_NAME: Final = "red_knot_python_semantic"
CRATE_ROOT: Final = Path(__file__).resolve().parent
MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest"
class MDTestRunner:
mdtest_executable: Path | None
console: Console
def __init__(self) -> None:
self.mdtest_executable = None
self.console = Console()
def _run_cargo_test(self, *, message_format: Literal["human", "json"]) -> str:
return subprocess.check_output(
[
"cargo",
"test",
"--package",
CRATE_NAME,
"--no-run",
"--color=always",
"--message-format",
message_format,
],
cwd=CRATE_ROOT,
env=dict(os.environ, CLI_COLOR="1"),
stderr=subprocess.STDOUT,
text=True,
)
def _recompile_tests(
self, status_message: str, *, message_on_success: bool = True
) -> bool:
with self.console.status(status_message):
# Run it with 'human' format in case there are errors:
try:
self._run_cargo_test(message_format="human")
except subprocess.CalledProcessError as e:
print(e.output)
return False
# Run it again with 'json' format to find the mdtest executable:
try:
json_output = self._run_cargo_test(message_format="json")
except subprocess.CalledProcessError as _:
# `cargo test` can still fail if something changed in between the two runs.
# Here we don't have a human-readable output, so just show a generic message:
self.console.print("[red]Error[/red]: Failed to compile tests")
return False
if json_output:
self._get_executable_path_from_json(json_output)
if message_on_success:
self.console.print("[dim]Tests compiled successfully[/dim]")
return True
def _get_executable_path_from_json(self, json_output: str) -> None:
for json_line in json_output.splitlines():
try:
data = json.loads(json_line)
except json.JSONDecodeError:
continue
if data.get("target", {}).get("name") == "mdtest":
self.mdtest_executable = Path(data["executable"])
break
else:
raise RuntimeError(
"Could not find mdtest executable after successful compilation"
)
def _run_mdtest(
self, arguments: list[str] | None = None, *, capture_output: bool = False
) -> subprocess.CompletedProcess:
assert self.mdtest_executable is not None
arguments = arguments or []
return subprocess.run(
[self.mdtest_executable, *arguments],
cwd=CRATE_ROOT,
env=dict(os.environ, CLICOLOR_FORCE="1"),
capture_output=capture_output,
text=True,
check=False,
)
def _run_mdtests_for_file(self, markdown_file: Path) -> None:
path_mangled = (
markdown_file.as_posix()
.replace("/", "_")
.replace("-", "_")
.removesuffix(".md")
)
test_name = f"mdtest__{path_mangled}"
output = self._run_mdtest(["--exact", test_name], capture_output=True)
if output.returncode == 0:
if "running 0 tests\n" in output.stdout:
self.console.log(
f"[yellow]Warning[/yellow]: No tests were executed with filter '{test_name}'"
)
else:
self.console.print(
f"Test for [bold green]{markdown_file}[/bold green] succeeded"
)
else:
self.console.print()
self.console.rule(
f"Test for [bold red]{markdown_file}[/bold red] failed",
style="gray",
)
self._print_trimmed_cargo_test_output(
output.stdout + output.stderr, test_name
)
def _print_trimmed_cargo_test_output(self, output: str, test_name: str) -> None:
# Skip 'cargo test' boilerplate at the beginning:
lines = output.splitlines()
start_index = 0
for i, line in enumerate(lines):
if f"{test_name} stdout" in line:
start_index = i
break
for line in lines[start_index + 1 :]:
if "MDTEST_TEST_FILTER" in line:
continue
if line.strip() == "-" * 50:
# Skip 'cargo test' boilerplate at the end
break
print(line)
def watch(self) -> Never:
self._recompile_tests("Compiling tests...", message_on_success=False)
self.console.print("[dim]Ready to watch for changes...[/dim]")
for changes in watch(CRATE_ROOT):
new_md_files = set()
changed_md_files = set()
rust_code_has_changed = False
for change, path_str in changes:
path = Path(path_str)
if path.suffix == ".rs":
rust_code_has_changed = True
continue
if path.suffix != ".md":
continue
try:
relative_path = Path(path).relative_to(MDTEST_DIR)
except ValueError:
continue
match change:
case Change.added:
# When saving a file, some editors (looking at you, Vim) might first
# save the file with a temporary name (e.g. `file.md~`) and then rename
# it to the final name. This creates a `deleted` and `added` change.
# We treat those files as `changed` here.
if (Change.deleted, path_str) in changes:
changed_md_files.add(relative_path)
else:
new_md_files.add(relative_path)
case Change.modified:
changed_md_files.add(relative_path)
case Change.deleted:
# No need to do anything when a Markdown test is deleted
pass
case _ as unreachable:
assert_never(unreachable)
if rust_code_has_changed:
if self._recompile_tests("Rust code has changed, recompiling tests..."):
self._run_mdtest()
elif new_md_files:
files = " ".join(file.as_posix() for file in new_md_files)
self._recompile_tests(
f"New Markdown test [yellow]{files}[/yellow] detected, recompiling tests..."
)
for path in new_md_files | changed_md_files:
self._run_mdtests_for_file(path)
def main() -> None:
try:
runner = MDTestRunner()
runner.watch()
except KeyboardInterrupt:
print()
if __name__ == "__main__":
main()

View File

@@ -1,141 +0,0 @@
version = 1
requires-python = ">=3.11"
[manifest]
requirements = [
{ name = "rich" },
{ name = "watchfiles" },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "rich"
version = "13.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "watchfiles"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 },
{ url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 },
{ url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 },
{ url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 },
{ url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 },
{ url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 },
{ url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 },
{ url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 },
{ url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 },
{ url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 },
{ url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 },
{ url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 },
{ url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 },
{ url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 },
{ url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 },
{ url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 },
{ url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 },
{ url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 },
{ url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 },
{ url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 },
{ url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 },
{ url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 },
{ url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 },
{ url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 },
{ url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 },
{ url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 },
{ url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
{ url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
{ url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
{ url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
{ url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
{ url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
{ url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
{ url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
{ url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
{ url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
{ url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
]

View File

@@ -11,7 +11,7 @@ x: Any = 1
x = "foo"
def f():
reveal_type(x) # revealed: Any | Literal["foo"]
reveal_type(x) # revealed: Any
```
## Aliased to a different name
@@ -25,7 +25,7 @@ x: RenamedAny = 1
x = "foo"
def f():
reveal_type(x) # revealed: Any | Literal["foo"]
reveal_type(x) # revealed: Any
```
## Shadowed class

View File

@@ -1,44 +0,0 @@
# Deferred annotations
## Deferred annotations in stubs always resolve
```pyi path=mod.pyi
def get_foo() -> Foo: ...
class Foo: ...
```
```py
from mod import get_foo
reveal_type(get_foo()) # revealed: Foo
```
## Deferred annotations in regular code fail
In (regular) source files, annotations are *not* deferred. This also tests that imports from
`__future__` that are not `annotations` are ignored.
```py
from __future__ import with_statement as annotations
# error: [unresolved-reference]
def get_foo() -> Foo: ...
class Foo: ...
reveal_type(get_foo()) # revealed: Unknown
```
## Deferred annotations in regular code with `__future__.annotations`
If `__future__.annotations` is imported, annotations *are* deferred.
```py
from __future__ import annotations
def get_foo() -> Foo: ...
class Foo: ...
reveal_type(get_foo()) # revealed: Foo
```

View File

@@ -36,7 +36,7 @@ def f():
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Unknown | Literal[0]
reveal_type(b1) # revealed: Literal[0]
# error: [invalid-type-form]
invalid1: Literal[3 + 4]

View File

@@ -100,7 +100,7 @@ def _(flag: bool):
foo_3: LiteralString = "foo" * 1_000_000_000
bar_3: str = foo_2 # fine
baz_1: str = repr(object())
baz_1: str = str()
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
baz_2: LiteralString = "baz" * 1_000_000_000

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(Unsupported or invalid type in a type expression) | Literal[int]
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression)
def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...

View File

@@ -49,12 +49,12 @@ reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
# TODO: homogeneous tuples, PEP-646 tuples
reveal_type(e) # revealed: @Todo(full tuple[...] support) | tuple[()]
reveal_type(f) # revealed: @Todo(full tuple[...] support) | tuple[Literal["42"], Literal[b"42"]]
reveal_type(g) # revealed: @Todo(full tuple[...] support) | tuple[Literal["42"], Literal[b"42"]]
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(f) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
# TODO: support more kinds of type expressions in annotations
reveal_type(h) # revealed: @Todo(full tuple[...] support) | tuple[list, list]
reveal_type(h) # revealed: @Todo(full tuple[...] support)
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]

View File

@@ -34,21 +34,21 @@ c_instance = C(1)
# TODO: should be `Literal["value set in __init__"]`, or `Unknown | Literal[…]` to allow
# assignments to this unannotated attribute from other scopes.
reveal_type(c_instance.pure_instance_variable1) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable1) # revealed: @Todo(instance attributes)
# TODO: should be `int`
reveal_type(c_instance.pure_instance_variable2) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable2) # revealed: @Todo(instance attributes)
# TODO: should be `bytes`
reveal_type(c_instance.pure_instance_variable3) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable3) # revealed: @Todo(instance attributes)
# TODO: should be `bool`
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(instance attributes)
# TODO: should be `str`
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
# mypy and pyright do not show an error here.
reveal_type(c_instance.pure_instance_variable5) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable5) # revealed: @Todo(instance attributes)
# TODO: If we choose to infer a precise `Literal[…]` type for the instance attribute (see
# above), this should be an error: incompatible types in assignment. If we choose to infer
@@ -74,7 +74,7 @@ c_instance.pure_instance_variable4 = False
# in general (we don't know what else happened to `c_instance` between the assignment and the use
# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably
# be `Literal[False]`.
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(instance attributes)
```
#### Variable declared in class body and declared/bound in `__init__`
@@ -91,7 +91,8 @@ class C:
c_instance = C()
reveal_type(c_instance.pure_instance_variable) # revealed: str
# TODO: should be `str`
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(instance attributes)
# TODO: we currently plan to emit a diagnostic here. Note that both mypy
# and pyright show no error in this case! So we may reconsider this in
@@ -102,7 +103,7 @@ reveal_type(C.pure_instance_variable) # revealed: str
# and pyright allow this.
C.pure_instance_variable = "overwritten on class"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_instance_variable` of type `str`"
# TODO: this should be an error (incompatible types in assignment)
c_instance.pure_instance_variable = 1
```
@@ -122,7 +123,7 @@ c_instance = C()
c_instance.set_instance_variable()
# TODO: should be `Literal["value set in method"]` or `Unknown | Literal[…]` (see above).
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(instance attributes)
# TODO: We already show an error here, but the message might be improved?
# error: [unresolved-attribute]
@@ -143,7 +144,8 @@ class C:
c_instance = C()
reveal_type(c_instance.pure_instance_variable) # revealed: str
# TODO: should be 'str'
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(instance attributes)
# TODO: mypy and pyright do not show an error here, but we plan to emit a diagnostic.
# The type could be changed to 'Unknown' if we decide to emit an error?
@@ -169,27 +171,22 @@ class C:
pure_class_variable1: ClassVar[str] = "value in class body"
pure_class_variable2: ClassVar = 1
def method(self):
# TODO: this should be an error
self.pure_class_variable1 = "value set through instance"
reveal_type(C.pure_class_variable1) # revealed: str
reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1]
# TODO: this should be `Literal[1]`, or `Unknown | Literal[1]`.
reveal_type(C.pure_class_variable2) # revealed: @Todo(Unsupported or invalid type in a type expression)
c_instance = C()
# It is okay to access a pure class variable on an instance.
reveal_type(c_instance.pure_class_variable1) # revealed: str
# TODO: This should be `str`. It is okay to access a pure class variable on an instance.
reveal_type(c_instance.pure_class_variable1) # revealed: @Todo(instance attributes)
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown | Literal[1]
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`"
# TODO: should raise an error. It is not allowed to reassign a pure class variable on an instance.
c_instance.pure_class_variable1 = "value set on instance"
C.pure_class_variable1 = "overwritten on class"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_class_variable1` of type `str`"
# TODO: should raise an error (incompatible types in assignment)
C.pure_class_variable1 = 1
class Subclass(C):
@@ -225,7 +222,7 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
c_instance = C()
# TODO: should be `Literal["overwritten on class"]`
reveal_type(c_instance.pure_class_variable) # revealed: @Todo(implicit instance attribute)
reveal_type(c_instance.pure_class_variable) # revealed: @Todo(instance attributes)
# TODO: should raise an error.
c_instance.pure_class_variable = "value set on instance"
@@ -242,37 +239,34 @@ attributes.
```py
class C:
variable_with_class_default1: str = "value in class body"
variable_with_class_default2 = 1
variable_with_class_default: str = "value in class body"
def instance_method(self):
self.variable_with_class_default1 = "value set in instance method"
self.variable_with_class_default = "value set in instance method"
reveal_type(C.variable_with_class_default1) # revealed: str
reveal_type(C.variable_with_class_default2) # revealed: Unknown | Literal[1]
reveal_type(C.variable_with_class_default) # revealed: str
c_instance = C()
reveal_type(c_instance.variable_with_class_default1) # revealed: str
reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Literal[1]
# TODO: should be `str`
reveal_type(c_instance.variable_with_class_default) # revealed: @Todo(instance attributes)
c_instance.variable_with_class_default1 = "value set on instance"
c_instance.variable_with_class_default = "value set on instance"
reveal_type(C.variable_with_class_default1) # revealed: str
reveal_type(C.variable_with_class_default) # revealed: str
# TODO: Could be Literal["value set on instance"], or still `str` if we choose not to
# narrow the type.
reveal_type(c_instance.variable_with_class_default1) # revealed: str
reveal_type(c_instance.variable_with_class_default) # revealed: @Todo(instance attributes)
C.variable_with_class_default1 = "overwritten on class"
C.variable_with_class_default = "overwritten on class"
# TODO: Could be `Literal["overwritten on class"]`, or still `str` if we choose not to
# narrow the type.
reveal_type(C.variable_with_class_default1) # revealed: str
reveal_type(C.variable_with_class_default) # revealed: str
# TODO: should still be `Literal["value set on instance"]`, or `str`.
reveal_type(c_instance.variable_with_class_default1) # revealed: str
reveal_type(c_instance.variable_with_class_default) # revealed: @Todo(instance attributes)
```
## Union of attributes
@@ -293,8 +287,8 @@ def _(flag: bool):
else:
x = 4
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
reveal_type(C1.x) # revealed: Literal[1, 2]
reveal_type(C2.x) # revealed: Literal[3, 4]
```
## Inherited class attributes
@@ -308,7 +302,7 @@ class A:
class B(A): ...
class C(B): ...
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
reveal_type(C.X) # revealed: Literal["foo"]
```
### Multiple inheritance
@@ -331,7 +325,7 @@ class A(B, C): ...
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]
reveal_type(A.X) # revealed: Literal[42]
```
## Unions with possibly unbound paths
@@ -353,7 +347,7 @@ def _(flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
reveal_type(C.x) # revealed: Literal[1, 3]
```
### Possibly-unbound within a class
@@ -376,7 +370,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3
# 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]
reveal_type(C.x) # revealed: Literal[1, 2, 3]
```
### Unions with all paths unbound
@@ -433,64 +427,6 @@ class Foo: ...
reveal_type(Foo.__class__) # revealed: Literal[type]
```
## Module attributes
```py path=mod.py
global_symbol: str = "a"
```
```py
import mod
reveal_type(mod.global_symbol) # revealed: str
mod.global_symbol = "b"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`"
mod.global_symbol = 1
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `global_symbol` of type `str`"
(_, mod.global_symbol) = (..., 1)
# TODO: this should be an error, but we do not understand list unpackings yet.
[_, mod.global_symbol] = [1, 2]
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# error: [invalid-assignment] "Object of type `int` is not assignable to attribute `global_symbol` of type `str`"
for mod.global_symbol in IntIterable():
pass
```
## Nested attributes
```py path=outer/__init__.py
```
```py path=outer/nested/__init__.py
```
```py path=outer/nested/inner.py
class Outer:
class Nested:
class Inner:
attr: int = 1
```
```py
import outer.nested.inner
reveal_type(outer.nested.inner.Outer.Nested.Inner.attr) # revealed: int
# error: [invalid-assignment]
outer.nested.inner.Outer.Nested.Inner.attr = "a"
```
## Literal types
### Function-literal attributes
@@ -501,8 +437,8 @@ functions are instances of that class:
```py path=a.py
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
reveal_type(f.__defaults__) # revealed: @Todo(instance attributes)
reveal_type(f.__kwdefaults__) # revealed: @Todo(instance attributes)
```
Some attributes are special-cased, however:
@@ -520,8 +456,8 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si
integers are instances of that class:
```py path=a.py
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).denominator) # revealed: @Todo(@property)
reveal_type((2).bit_length) # revealed: @Todo(instance attributes)
reveal_type((2).denominator) # revealed: @Todo(instance attributes)
```
Some attributes are special-cased, however:
@@ -537,8 +473,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
bols are instances of that class:
```py path=a.py
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
reveal_type(True.__and__) # revealed: @Todo(instance attributes)
reveal_type(False.__or__) # revealed: @Todo(instance attributes)
```
Some attributes are special-cased, however:
@@ -553,8 +489,8 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
```py
reveal_type(b"foo".join) # revealed: @Todo(bound method)
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
reveal_type(b"foo".join) # revealed: @Todo(instance attributes)
reveal_type(b"foo".endswith) # revealed: @Todo(instance attributes)
```
## References

View File

@@ -50,44 +50,46 @@ reveal_type(b | b) # revealed: Literal[False]
## Arithmetic with a variable
```py
def _(a: bool):
def lhs_is_int(x: int):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
a = True
b = False
def rhs_is_int(x: int):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_int(x: int):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def lhs_is_bool(x: bool):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def rhs_is_bool(x: bool):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def both_are_bool(x: bool, y: bool):
reveal_type(x + y) # revealed: int
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
def rhs_is_bool(x: bool):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
reveal_type(x + y) # revealed: int
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
```

View File

@@ -262,8 +262,7 @@ class A:
class B:
__add__ = A()
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
reveal_type(B() + B()) # revealed: Unknown | int
reveal_type(B() + B()) # revealed: int
```
## Integration test: numbers from typeshed

View File

@@ -1,18 +1,9 @@
# Boundness and declaredness: public uses
This document demonstrates how type-inference and diagnostics work for *public* uses of a symbol,
This document demonstrates how type-inference and diagnostics works for *public* uses of a symbol,
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
of other scopes). If there is an inferred type in addition (i.e. if we also see bindings for this
symbol, not just declarations), we use `T_decl | T_decl & T_inf` as the public type, which
simplifies to `T_decl` for `T_inf = Unknown` (the unbound case).
[TODO: more explanation]
If a symbol has no declared type, we use the union of `Unknown` with the inferred type as the public
type. If there is no declaration, then the symbol can be reassigned to any type from another scope;
the union with `Unknown` reflects that its type must at least be as large as the type of the
assigned value, but could be arbitrarily larger.
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
We test the whole matrix of possible boundness and declaredness states. The current behavior is
summarized in the following table, while the tests below demonstrate each case. Note that some of
@@ -21,11 +12,11 @@ this behavior is questionable and might change in the future. See the TODOs in `
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
"undeclared-and-possibly-unbound" cases (marked with a "?").
| **Public type** | declared | possibly-undeclared | undeclared |
| ---------------- | -------------------------- | ------------------- | ------------------ |
| bound | `T_decl \| T_decl & T_inf` | `T_decl \| T_inf` | `Unknown \| T_inf` |
| possibly-unbound | `T_decl \| T_decl & T_inf` | `T_decl \| T_inf` | `Unknown \| T_inf` |
| unbound | `T_decl` | `T_decl` | `Unknown` |
| **Public type** | declared | possibly-undeclared | undeclared |
| ---------------- | ------------ | -------------------------- | ------------ |
| bound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
| unbound | `T_declared` | `T_declared` | `Unknown` |
| **Diagnostic** | declared | possibly-undeclared | undeclared |
| ---------------- | -------- | ------------------------- | ------------------- |
@@ -41,27 +32,17 @@ If a symbol has a declared type (`int`), we use that even if there is a more pre
(`Literal[1]`), or a conflicting inferred type (`Literal[2]`):
```py path=mod.py
from typing import Any
def any() -> Any: ...
a: int = 1
x: int = 1
# error: [invalid-assignment]
b: str = 2
c: Any = 3
d: int = any()
y: str = 2
```
```py
from mod import a, b, c, d
from mod import x, y
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
reveal_type(c) # revealed: Any | Literal[3]
reveal_type(d) # revealed: int
reveal_type(x) # revealed: int
reveal_type(y) # revealed: str
```
### Declared and possibly unbound
@@ -70,31 +51,21 @@ If a symbol is declared and *possibly* unbound, we trust that other module and u
without raising an error.
```py path=mod.py
from typing import Any
def any() -> Any: ...
def flag() -> bool: ...
a: int
b: str
c: Any
d: int
x: int
y: str
if flag:
a = 1
x = 1
# error: [invalid-assignment]
b = 2
c = 3
d = any()
y = 2
```
```py
from mod import a, b, c, d
from mod import x, y
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
reveal_type(c) # revealed: Any | Literal[3]
reveal_type(d) # revealed: int
reveal_type(x) # revealed: int
reveal_type(y) # revealed: str
```
### Declared and unbound
@@ -103,17 +74,13 @@ Similarly, if a symbol is declared but unbound, we do not raise an error. We tru
is available somehow and simply use the declared type.
```py path=mod.py
from typing import Any
a: int
b: Any
x: int
```
```py
from mod import a, b
from mod import x
reveal_type(a) # revealed: int
reveal_type(b) # revealed: Any
reveal_type(x) # revealed: int
```
## Possibly undeclared
@@ -128,56 +95,45 @@ from typing import Any
def flag() -> bool: ...
a = 1
b = 2
c = 3
x = 1
y = 2
if flag():
a: int
b: Any
x: Any
# error: [invalid-declaration]
c: str
y: str
```
```py
from mod import a, b, c
from mod import x, y
reveal_type(a) # revealed: int
reveal_type(b) # revealed: Literal[2] | Any
reveal_type(c) # revealed: Literal[3] | Unknown
# External modifications of `a` that violate the declared type are not allowed:
# error: [invalid-assignment]
a = None
reveal_type(x) # revealed: Literal[1] | Any
reveal_type(y) # revealed: Literal[2] | Unknown
```
### Possibly undeclared and possibly unbound
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
inferred types. This case is interesting because the "possibly declared" definition might not be the
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import`
error for both `a` and `b`:
same as the "possibly bound" definition (symbol `y`). Note that we raise a `possibly-unbound-import`
error for both `x` and `y`:
```py path=mod.py
def flag() -> bool: ...
if flag():
a: Any = 1
b = 2
x: Any = 1
y = 2
else:
b: str
y: str
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
from mod import a, b
from mod import x, y
reveal_type(a) # revealed: Literal[1] | Any
reveal_type(b) # revealed: Literal[2] | str
# External modifications of `b` that violate the declared type are not allowed:
# error: [invalid-assignment]
b = None
reveal_type(x) # revealed: Literal[1] | Any
reveal_type(y) # revealed: Literal[2] | str
```
### Possibly undeclared and unbound
@@ -189,36 +145,31 @@ seems inconsistent when compared to the case just above.
def flag() -> bool: ...
if flag():
a: int
x: int
```
```py
# TODO: this should raise an error. Once we fix this, update the section description and the table
# on top of this document.
from mod import a
from mod import x
reveal_type(a) # revealed: int
# External modifications to `a` that violate the declared type are not allowed:
# error: [invalid-assignment]
a = None
reveal_type(x) # revealed: int
```
## Undeclared
### Undeclared but bound
We use the inferred type as the public type, if a symbol has no declared type.
```py path=mod.py
a = 1
x = 1
```
```py
from mod import a
from mod import x
reveal_type(a) # revealed: Unknown | Literal[1]
# All external modifications of `a` are allowed:
a = None
reveal_type(x) # revealed: Literal[1]
```
### Undeclared and possibly unbound
@@ -230,18 +181,15 @@ inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" cas
def flag() -> bool: ...
if flag:
a = 1
x = 1
```
```py
# TODO: this should raise an error. Once we fix this, update the section description and the table
# on top of this document.
from mod import a
from mod import x
reveal_type(a) # revealed: Unknown | Literal[1]
# All external modifications of `a` are allowed:
a = None
reveal_type(x) # revealed: Literal[1]
```
### Undeclared and unbound
@@ -250,15 +198,12 @@ If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
```py path=mod.py
if False:
a: int = 1
x: int = 1
```
```py
# error: [unresolved-import]
from mod import a
from mod import x
reveal_type(a) # revealed: Unknown
# Modifications allowed in this case:
a = None
reveal_type(x) # revealed: Unknown
```

View File

@@ -52,7 +52,7 @@ class NonCallable:
__call__ = 1
a = NonCallable()
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
# error: "Object of type `NonCallable` is not callable"
reveal_type(a()) # revealed: Unknown
```

View File

@@ -1,151 +0,0 @@
# Comprehensions
## Basic comprehensions
```py
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# revealed: int
[reveal_type(x) for x in IntIterable()]
class IteratorOfIterables:
def __next__(self) -> IntIterable:
return IntIterable()
class IterableOfIterables:
def __iter__(self) -> IteratorOfIterables:
return IteratorOfIterables()
# revealed: tuple[int, IntIterable]
[reveal_type((x, y)) for y in IterableOfIterables() for x in y]
# revealed: int
{reveal_type(x): 0 for x in IntIterable()}
# revealed: int
{0: reveal_type(x) for x in IntIterable()}
```
## Nested comprehension
```py
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# TODO: This could be a `tuple[int, int]` if we model that `y` can not be modified in the outer comprehension scope
# revealed: tuple[int, Unknown | int]
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]
```
## Comprehension referencing outer comprehension
```py
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class IteratorOfIterables:
def __next__(self) -> IntIterable:
return IntIterable()
class IterableOfIterables:
def __iter__(self) -> IteratorOfIterables:
return IteratorOfIterables()
# TODO: This could be a `tuple[int, int]` (see above)
# revealed: tuple[int, Unknown | IntIterable]
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]
```
## Comprehension with unbound iterable
Iterating over an unbound iterable yields `Unknown`:
```py
# error: [unresolved-reference] "Name `x` used when not defined"
# revealed: Unknown
[reveal_type(z) for z in x]
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# error: [not-iterable] "Object of type `int` is not iterable"
# revealed: tuple[int, Unknown]
[reveal_type((x, z)) for x in IntIterable() for z in x]
```
## Starred expressions
Starred expressions must be iterable
```py
class NotIterable: ...
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator: ...
# This is fine:
x = [*Iterable()]
# error: [not-iterable] "Object of type `NotIterable` is not iterable"
y = [*NotIterable()]
```
## Async comprehensions
### Basic
```py
class AsyncIterator:
async def __anext__(self) -> int:
return 42
class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
```
### Invalid async comprehension
This tests that we understand that `async` comprehensions do *not* work according to the synchronous
iteration protocol
```py
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
```

View File

@@ -1,43 +0,0 @@
# Comprehensions with invalid syntax
```py
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# Missing 'in' keyword.
# It's reasonably clear here what they *meant* to write,
# so we'll still infer the correct type:
# error: [invalid-syntax] "Expected 'in', found name"
# revealed: int
[reveal_type(a) for a IntIterable()]
# Missing iteration variable
# error: [invalid-syntax] "Expected an identifier, but found a keyword 'in' that cannot be used here"
# error: [invalid-syntax] "Expected 'in', found name"
# error: [unresolved-reference]
# revealed: Unknown
[reveal_type(b) for in IntIterable()]
# Missing iterable
# error: [invalid-syntax] "Expected an expression"
# revealed: Unknown
[reveal_type(c) for c in]
# Missing 'in' keyword and missing iterable
# error: [invalid-syntax] "Expected 'in', found ']'"
# revealed: Unknown
[reveal_type(d) for d]
```

View File

@@ -110,8 +110,10 @@ python-version = "3.10"
from typing_extensions import assert_type
def _(a: str | int):
assert_type(a, str | int)
assert_type(a, int | str)
assert_type(a, str | int) # fine
# TODO: Order-independent union handling in type equivalence
assert_type(a, int | str) # error: [type-assertion-failure]
```
## Intersections
@@ -133,6 +135,8 @@ def _(a: A):
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
reveal_type(a) # revealed: A & B & ~C & ~D
assert_type(a, Intersection[A, B, Not[C], Not[D]])
assert_type(a, Intersection[B, A, Not[D], Not[C]])
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
# TODO: Order-independent intersection handling in type equivalence
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
```

View File

@@ -5,7 +5,7 @@
```py
def _(flag: bool):
class A:
always_bound: int = 1
always_bound = 1
if flag:
union = 1
@@ -13,21 +13,14 @@ def _(flag: bool):
union = "abc"
if flag:
union_declared: int = 1
else:
union_declared: str = "abc"
possibly_unbound = "abc"
if flag:
possibly_unbound: str = "abc"
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.always_bound) # revealed: int
reveal_type(A.union) # revealed: Unknown | Literal[1, "abc"]
reveal_type(A.union_declared) # revealed: int | str
reveal_type(A.union) # revealed: Literal[1, "abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: str
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown

View File

@@ -55,7 +55,7 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
## Evaluates to builtin
```py path=a.py
redefined_builtin_bool: type[bool] = bool
redefined_builtin_bool = bool
def my_bool(x) -> bool:
return True

View File

@@ -172,10 +172,10 @@ class IntUnion:
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
reveal_type(len(Auto())) # revealed: int
reveal_type(len(Int())) # revealed: int
reveal_type(len(Int())) # revealed: Literal[2]
reveal_type(len(Str())) # revealed: int
reveal_type(len(Tuple())) # revealed: int
reveal_type(len(IntUnion())) # revealed: int
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
```
### Negative integers

View File

@@ -17,10 +17,10 @@ box: MyBox[int] = MyBox(5)
# TODO should emit a diagnostic here (str is not assignable to int)
wrong_innards: MyBox[int] = MyBox("five")
# TODO reveal int, do not leak the typevar
reveal_type(box.data) # revealed: T
# TODO reveal int
reveal_type(box.data) # revealed: @Todo(instance attributes)
reveal_type(MyBox.box_model_number) # revealed: Unknown | Literal[695]
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
```
## Subclassing
@@ -39,9 +39,7 @@ class MySecureBox[T](MyBox[T]): ...
secure_box: MySecureBox[int] = MySecureBox(5)
reveal_type(secure_box) # revealed: MySecureBox
# TODO reveal int
# The @Todo(…) is misleading here. We currently treat `MyBox[T]` as a dynamic base class because we
# don't understand generics and therefore infer `Unknown` for the `MyBox[T]` base of `MySecureBox[T]`.
reveal_type(secure_box.data) # revealed: @Todo(instance attribute on class with dynamic base)
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
```
## Cyclical class definition

View File

@@ -1,70 +1,8 @@
# Builtins
## Importing builtin module
Builtin symbols can be explicitly imported:
# Importing builtin module
```py
import builtins
reveal_type(builtins.chr) # revealed: Literal[chr]
```
## Implicit use of builtin
Or used implicitly:
```py
reveal_type(chr) # revealed: Literal[chr]
reveal_type(str) # revealed: Literal[str]
```
## Builtin symbol from custom typeshed
If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the
builtins from the "actual" vendored typeshed:
```toml
[environment]
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/builtins.pyi
class Custom: ...
custom_builtin: Custom
```
```pyi path=/typeshed/stdlib/typing_extensions.pyi
def reveal_type(obj, /): ...
```
```py
reveal_type(custom_builtin) # revealed: Custom
# error: [unresolved-reference]
reveal_type(str) # revealed: Unknown
```
## Unknown builtin (later defined)
`foo` has a type of `Unknown` in this example, as it relies on `bar` which has not been defined at
that point:
```toml
[environment]
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/builtins.pyi
foo = bar
bar = 1
```
```pyi path=/typeshed/stdlib/typing_extensions.pyi
def reveal_type(obj, /): ...
```
```py
reveal_type(foo) # revealed: Unknown
x = builtins.chr
reveal_type(x) # revealed: Literal[chr]
```

View File

@@ -23,8 +23,8 @@ reveal_type(y)
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
from maybe_unbound import x, y
reveal_type(x) # revealed: Unknown | Literal[3]
reveal_type(y) # revealed: Unknown | Literal[3]
reveal_type(x) # revealed: Literal[3]
reveal_type(y) # revealed: Literal[3]
```
## Maybe unbound annotated
@@ -52,7 +52,7 @@ Importing an annotated name prefers the declared type over the inferred type:
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
from maybe_unbound_annotated import x, y
reveal_type(x) # revealed: Unknown | Literal[3]
reveal_type(x) # revealed: Literal[3]
reveal_type(y) # revealed: int
```

View File

@@ -9,7 +9,7 @@ reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
b: int = 42
b = 42
```
```py path=a/b.py
@@ -20,11 +20,11 @@ b: int = 42
```py
from a import b
reveal_type(b) # revealed: int
reveal_type(b) # revealed: Literal[42]
```
```py path=a/__init__.py
b: int = 42
b = 42
```
```py path=a/b.py
@@ -41,7 +41,7 @@ reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
b: int = 42
b = 42
```
```py path=a/b.py
@@ -60,13 +60,13 @@ sees the submodule as the value of `b` instead of the integer.
from a import b
import a.b
# Python would say `int` for `b`
# Python would say `Literal[42]` for `b`
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
b: int = 42
b = 42
```
```py path=a/b.py

View File

@@ -20,12 +20,12 @@ from a import b.c
# TODO: Should these be inferred as Unknown?
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.c) # revealed: int
reveal_type(b.c) # revealed: Literal[1]
```
```py path=a/__init__.py
```
```py path=a/b.py
c: int = 1
c = 1
```

View File

@@ -17,13 +17,13 @@ reveal_type(X) # revealed: Unknown
```
```py path=package/foo.py
X: int = 42
X = 42
```
```py path=package/bar.py
from .foo import X
reveal_type(X) # revealed: int
reveal_type(X) # revealed: Literal[42]
```
## Dotted
@@ -32,25 +32,25 @@ reveal_type(X) # revealed: int
```
```py path=package/foo/bar/baz.py
X: int = 42
X = 42
```
```py path=package/bar.py
from .foo.bar.baz import X
reveal_type(X) # revealed: int
reveal_type(X) # revealed: Literal[42]
```
## Bare to package
```py path=package/__init__.py
X: int = 42
X = 42
```
```py path=package/bar.py
from . import X
reveal_type(X) # revealed: int
reveal_type(X) # revealed: Literal[42]
```
## Non-existent + bare to package
@@ -66,11 +66,11 @@ reveal_type(X) # revealed: Unknown
```py path=package/__init__.py
from .foo import X
reveal_type(X) # revealed: int
reveal_type(X) # revealed: Literal[42]
```
```py path=package/foo.py
X: int = 42
X = 42
```
## Non-existent + dunder init
@@ -87,13 +87,13 @@ reveal_type(X) # revealed: Unknown
```
```py path=package/foo.py
X: int = 42
X = 42
```
```py path=package/subpackage/subsubpackage/bar.py
from ...foo import X
reveal_type(X) # revealed: int
reveal_type(X) # revealed: Literal[42]
```
## Unbound symbol
@@ -117,13 +117,13 @@ reveal_type(x) # revealed: Unknown
```
```py path=package/foo.py
X: int = 42
X = 42
```
```py path=package/bar.py
from . import foo
reveal_type(foo.X) # revealed: int
reveal_type(foo.X) # revealed: Literal[42]
```
## Non-existent + bare to module
@@ -152,7 +152,7 @@ submodule via the attribute on its parent package.
```
```py path=package/foo.py
X: int = 42
X = 42
```
```py path=package/bar.py

View File

@@ -3,16 +3,17 @@
## Expression
```py
from typing_extensions import Literal
x = 0
y = str()
z = False
def _(x: Literal[0], y: str, z: Literal[False]):
reveal_type(f"hello") # revealed: Literal["hello"]
reveal_type(f"h {x}") # revealed: Literal["h 0"]
reveal_type("one " f"single " f"literal") # revealed: Literal["one single literal"]
reveal_type("first " f"second({x})" f" third") # revealed: Literal["first second(0) third"]
reveal_type(f"-{y}-") # revealed: str
reveal_type(f"-{y}-" f"--" "--") # revealed: str
reveal_type(f"{z} == {False} is {True}") # revealed: Literal["False == False is True"]
reveal_type(f"hello") # revealed: Literal["hello"]
reveal_type(f"h {x}") # revealed: Literal["h 0"]
reveal_type("one " f"single " f"literal") # revealed: Literal["one single literal"]
reveal_type("first " f"second({x})" f" third") # revealed: Literal["first second(0) third"]
reveal_type(f"-{y}-") # revealed: str
reveal_type(f"-{y}-" f"--" "--") # revealed: str
reveal_type(f"{z} == {False} is {True}") # revealed: Literal["False == False is True"]
```
## Conversion Flags

View File

@@ -109,9 +109,9 @@ reveal_type(x)
def _(flag: bool):
class NotIterable:
if flag:
__iter__: int = 1
__iter__ = 1
else:
__iter__: None = None
__iter__ = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
@@ -135,7 +135,7 @@ for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
class NotIterable:
def __getitem__(self, key: int) -> int:
return 42
__iter__: None = None
__iter__ = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass

View File

@@ -1,95 +0,0 @@
# Custom typeshed
The `environment.typeshed` configuration option can be used to specify a custom typeshed directory
for Markdown-based tests. Custom typeshed stubs can then be placed in the specified directory using
fenced code blocks with language `pyi`, and will be used instead of the vendored copy of typeshed.
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
custom typeshed root. If no such file is created explicitly, it will be automatically created with
entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python versions.
## Basic example (auto-generated `VERSIONS` file)
First, we specify `/typeshed` as the custom typeshed directory:
```toml
[environment]
typeshed = "/typeshed"
```
We can then place custom stub files in `/typeshed/stdlib`, for example:
```pyi path=/typeshed/stdlib/builtins.pyi
class BuiltinClass: ...
builtin_symbol: BuiltinClass
```
```pyi path=/typeshed/stdlib/sys/__init__.pyi
version = "my custom Python"
```
And finally write a normal Python code block that makes use of the custom stubs:
```py
b: BuiltinClass = builtin_symbol
class OtherClass: ...
o: OtherClass = builtin_symbol # error: [invalid-assignment]
# Make sure that 'sys' has a proper entry in the auto-generated 'VERSIONS' file
import sys
```
## Custom `VERSIONS` file
If we want to specify a custom `VERSIONS` file, we can do so by creating a fenced code block with
language `text`. In the following test, we set the Python version to `3.10` and then make sure that
we can *not* import `new_module` with a version requirement of `3.11-`:
```toml
[environment]
python-version = "3.10"
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/old_module.pyi
class OldClass: ...
```
```pyi path=/typeshed/stdlib/new_module.pyi
class NewClass: ...
```
```text path=/typeshed/stdlib/VERSIONS
old_module: 3.0-
new_module: 3.11-
```
```py
from old_module import OldClass
# error: [unresolved-import] "Cannot resolve import `new_module`"
from new_module import NewClass
```
## Using `reveal_type` with a custom typeshed
When providing a custom typeshed directory, basic things like `reveal_type` will stop working
because we rely on being able to import it from `typing_extensions`. The actual definition of
`reveal_type` in typeshed is slightly involved (depends on generics, `TypeVar`, etc.), but a very
simple untyped definition is enough to make `reveal_type` work in tests:
```toml
[environment]
typeshed = "/typeshed"
```
```pyi path=/typeshed/stdlib/typing_extensions.pyi
def reveal_type(obj, /): ...
```
```py
reveal_type(()) # revealed: tuple[()]
```

View File

@@ -396,10 +396,11 @@ class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
class Bar(Foo): ...
# Avoid emitting the errors for these. The classes have cyclic superclasses,
# TODO: can we avoid emitting the errors for these?
# The classes have cyclic superclasses,
# but are not themselves cyclic...
class Baz(Bar, BarCycle): ...
class Spam(Baz): ...
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
class Spam(Baz): ... # error: [cyclic-class-definition]
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]

View File

@@ -199,20 +199,24 @@ def f(
if isinstance(a, bool):
reveal_type(a) # revealed: Never
else:
reveal_type(a) # revealed: P & AlwaysTruthy
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(a) # revealed: P & AlwaysTruthy & ~bool
if isinstance(b, bool):
reveal_type(b) # revealed: Never
else:
reveal_type(b) # revealed: P & AlwaysFalsy
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(b) # revealed: P & AlwaysFalsy & ~bool
if isinstance(c, bool):
reveal_type(c) # revealed: Never
else:
reveal_type(c) # revealed: P & ~AlwaysTruthy
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(c) # revealed: P & ~AlwaysTruthy & ~bool
if isinstance(d, bool):
reveal_type(d) # revealed: Never
else:
reveal_type(d) # revealed: P & ~AlwaysFalsy
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(d) # revealed: P & ~AlwaysFalsy & ~bool
```

View File

@@ -246,31 +246,3 @@ def _(x: type, y: type[int]):
if issubclass(x, y):
reveal_type(x) # revealed: type[int]
```
### Disjoint `type[]` types are narrowed to `Never`
Here, `type[UsesMeta1]` and `type[UsesMeta2]` are disjoint because a common subclass of `UsesMeta1`
and `UsesMeta2` could only exist if a common subclass of their metaclasses could exist. This is
known to be impossible due to the fact that `Meta1` is marked as `@final`.
```py
from typing import final
@final
class Meta1(type): ...
class Meta2(type): ...
class UsesMeta1(metaclass=Meta1): ...
class UsesMeta2(metaclass=Meta2): ...
def _(x: type[UsesMeta1], y: type[UsesMeta2]):
if issubclass(x, y):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: type[UsesMeta1]
if issubclass(y, x):
reveal_type(y) # revealed: Never
else:
reveal_type(y) # revealed: type[UsesMeta2]
```

View File

@@ -99,9 +99,9 @@ def _(x: str | int):
class A: ...
class B: ...
def _(x: A | B):
alias_for_type = type
alias_for_type = type
def _(x: A | B):
if alias_for_type(x) is A:
reveal_type(x) # revealed: A
```

View File

@@ -31,7 +31,7 @@ type IntOrStr = int | str
# TODO: This should either fall back to the specified type from typeshed,
# which is `Any`, or be the actual type of the runtime value expression
# `int | str`, i.e. `types.UnionType`.
reveal_type(IntOrStr.__value__) # revealed: @Todo(@property)
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
```
## Invalid assignment
@@ -74,22 +74,5 @@ type ListOrSet[T] = list[T] | set[T]
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
# as specified in the `typeshed` stubs.
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(@property)
```
## `TypeAliasType` properties
Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type
```py
from knot_extensions import static_assert, is_equivalent_to, is_disjoint_from, TypeOf
type Alias1 = int
type Alias2 = int
type TypeAliasType1 = TypeOf[Alias1]
type TypeAliasType2 = TypeOf[Alias2]
static_assert(not is_equivalent_to(TypeAliasType1, TypeAliasType2))
static_assert(is_disjoint_from(TypeAliasType1, TypeAliasType2))
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
```

View File

@@ -10,10 +10,10 @@ def returns_bool() -> bool:
return True
if returns_bool():
chr: int = 1
chr = 1
def f():
reveal_type(chr) # revealed: Literal[chr] | int
reveal_type(chr) # revealed: Literal[chr] | Literal[1]
```
## Conditionally global or builtin, with annotation

View File

@@ -63,7 +63,7 @@ reveal_type(typing.__class__) # revealed: Literal[ModuleType]
# TODO: needs support for attribute access on instances, properties and generics;
# should be `dict[str, Any]`
reveal_type(typing.__dict__) # revealed: @Todo(@property)
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
```
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
@@ -95,8 +95,8 @@ from foo import __dict__ as foo_dict
# TODO: needs support for attribute access on instances, properties, and generics;
# should be `dict[str, Any]` for both of these:
reveal_type(foo.__dict__) # revealed: @Todo(@property)
reveal_type(foo_dict) # revealed: @Todo(@property)
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
```
## Conditionally global or `ModuleType` attribute

View File

@@ -6,7 +6,7 @@
def f():
x = 1
def g():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: Literal[1]
```
## Two levels up
@@ -16,7 +16,7 @@ def f():
x = 1
def g():
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: Literal[1]
```
## Skips class scope
@@ -28,7 +28,7 @@ def f():
class C:
x = 2
def g():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: Literal[1]
```
## Skips annotation-only assignment
@@ -41,16 +41,5 @@ def f():
# name is otherwise not defined; maybe should be an error?
x: int
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -17,8 +17,8 @@ class C:
x = 2
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
reveal_type(C.x) # revealed: Unknown | Literal[2]
reveal_type(C.y) # revealed: Unknown | Literal[1]
reveal_type(C.x) # revealed: Literal[2]
reveal_type(C.y) # revealed: Literal[1]
```
## Possibly unbound in class and global scope
@@ -37,7 +37,7 @@ class C:
# error: [possibly-unresolved-reference]
y = x
reveal_type(C.y) # revealed: Unknown | Literal[1, "abc"]
reveal_type(C.y) # revealed: Literal[1, "abc"]
```
## Unbound function local

View File

@@ -182,34 +182,3 @@ class C(A, B): ...
# False negative: [incompatible-slots]
class A(int, str): ...
```
### Diagnostic if `__slots__` is externally modified
We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol
is not declared — a case in which we union with `Unknown` for other public symbols. The reason for
this is that `__slots__` has a special handling in the runtime. Modifying it externally is actually
allowed, but those changes do not take effect. If you have a class `C` with `__slots__ = ("foo",)`
and externally set `C.__slots__ = ("bar",)`, you still can't access `C.bar`. And you can still
access `C.foo`. We therefore issue a diagnostic for such assignments:
```py
class A:
__slots__ = ("a",)
# Modifying `__slots__` from within the class body is fine:
__slots__ = ("a", "b")
# No `Unknown` here:
reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
# But modifying it externally is not:
# error: [invalid-assignment]
A.__slots__ = ("a",)
# error: [invalid-assignment]
A.__slots__ = ("a", "b_new")
# error: [invalid-assignment]
A.__slots__ = ("a", "b", "c")
```

View File

@@ -11,7 +11,7 @@ version:
import sys
if sys.version_info >= (3, 9):
SomeFeature: str = "available"
SomeFeature = "available"
```
If we can statically determine that the condition is always true, then we can also understand that
@@ -21,7 +21,7 @@ If we can statically determine that the condition is always true, then we can al
from module1 import SomeFeature
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
reveal_type(SomeFeature) # revealed: str
reveal_type(SomeFeature) # revealed: Literal["available"]
```
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used

View File

@@ -31,7 +31,7 @@ def _(n: int):
## Slices
```py
b: bytes = b"\x00abc\xff"
b = b"\x00abc\xff"
reveal_type(b[0:2]) # revealed: Literal[b"\x00a"]
reveal_type(b[-3:]) # revealed: Literal[b"bc\xff"]

View File

@@ -14,8 +14,7 @@ a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscri
class NotSubscriptable:
__getitem__ = None
# error: "Method `__getitem__` of type `Unknown | None` is not callable on object of type `NotSubscriptable`"
a = NotSubscriptable()[0]
a = NotSubscriptable()[0] # error: "Method `__getitem__` of type `None` is not callable on object of type `NotSubscriptable`"
```
## Valid getitem

View File

@@ -28,52 +28,52 @@ def _(n: int):
## Slices
```py
s = "abcde"
reveal_type(s[0:0]) # revealed: Literal[""]
reveal_type(s[0:1]) # revealed: Literal["a"]
reveal_type(s[0:2]) # revealed: Literal["ab"]
reveal_type(s[0:5]) # revealed: Literal["abcde"]
reveal_type(s[0:6]) # revealed: Literal["abcde"]
reveal_type(s[1:3]) # revealed: Literal["bc"]
reveal_type(s[-3:5]) # revealed: Literal["cde"]
reveal_type(s[-4:-2]) # revealed: Literal["bc"]
reveal_type(s[-10:10]) # revealed: Literal["abcde"]
reveal_type(s[0:]) # revealed: Literal["abcde"]
reveal_type(s[2:]) # revealed: Literal["cde"]
reveal_type(s[5:]) # revealed: Literal[""]
reveal_type(s[:2]) # revealed: Literal["ab"]
reveal_type(s[:0]) # revealed: Literal[""]
reveal_type(s[:2]) # revealed: Literal["ab"]
reveal_type(s[:10]) # revealed: Literal["abcde"]
reveal_type(s[:]) # revealed: Literal["abcde"]
reveal_type(s[::-1]) # revealed: Literal["edcba"]
reveal_type(s[::2]) # revealed: Literal["ace"]
reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"]
reveal_type(s[::-2]) # revealed: Literal["eca"]
reveal_type(s[-1::-3]) # revealed: Literal["eb"]
reveal_type(s[None:2:None]) # revealed: Literal["ab"]
reveal_type(s[1:None:1]) # revealed: Literal["bcde"]
reveal_type(s[None:None:None]) # revealed: Literal["abcde"]
start = 1
stop = None
step = 2
reveal_type(s[start:stop:step]) # revealed: Literal["bd"]
reveal_type(s[False:True]) # revealed: Literal["a"]
reveal_type(s[True:3]) # revealed: Literal["bc"]
s[0:4:0] # error: [zero-stepsize-in-slice]
s[:4:0] # error: [zero-stepsize-in-slice]
s[0::0] # error: [zero-stepsize-in-slice]
s[::0] # error: [zero-stepsize-in-slice]
def _(m: int, n: int, s2: str):
s = "abcde"
reveal_type(s[0:0]) # revealed: Literal[""]
reveal_type(s[0:1]) # revealed: Literal["a"]
reveal_type(s[0:2]) # revealed: Literal["ab"]
reveal_type(s[0:5]) # revealed: Literal["abcde"]
reveal_type(s[0:6]) # revealed: Literal["abcde"]
reveal_type(s[1:3]) # revealed: Literal["bc"]
reveal_type(s[-3:5]) # revealed: Literal["cde"]
reveal_type(s[-4:-2]) # revealed: Literal["bc"]
reveal_type(s[-10:10]) # revealed: Literal["abcde"]
reveal_type(s[0:]) # revealed: Literal["abcde"]
reveal_type(s[2:]) # revealed: Literal["cde"]
reveal_type(s[5:]) # revealed: Literal[""]
reveal_type(s[:2]) # revealed: Literal["ab"]
reveal_type(s[:0]) # revealed: Literal[""]
reveal_type(s[:2]) # revealed: Literal["ab"]
reveal_type(s[:10]) # revealed: Literal["abcde"]
reveal_type(s[:]) # revealed: Literal["abcde"]
reveal_type(s[::-1]) # revealed: Literal["edcba"]
reveal_type(s[::2]) # revealed: Literal["ace"]
reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"]
reveal_type(s[::-2]) # revealed: Literal["eca"]
reveal_type(s[-1::-3]) # revealed: Literal["eb"]
reveal_type(s[None:2:None]) # revealed: Literal["ab"]
reveal_type(s[1:None:1]) # revealed: Literal["bcde"]
reveal_type(s[None:None:None]) # revealed: Literal["abcde"]
start = 1
stop = None
step = 2
reveal_type(s[start:stop:step]) # revealed: Literal["bd"]
reveal_type(s[False:True]) # revealed: Literal["a"]
reveal_type(s[True:3]) # revealed: Literal["bc"]
s[0:4:0] # error: [zero-stepsize-in-slice]
s[:4:0] # error: [zero-stepsize-in-slice]
s[0::0] # error: [zero-stepsize-in-slice]
s[::0] # error: [zero-stepsize-in-slice]
substring1 = s[m:n]
# TODO: Support overloads... Should be `LiteralString`
reveal_type(substring1) # revealed: @Todo(return type)

View File

@@ -23,51 +23,51 @@ reveal_type(b) # revealed: Unknown
## Slices
```py
t = (1, "a", None, b"b")
reveal_type(t[0:0]) # revealed: tuple[()]
reveal_type(t[0:1]) # revealed: tuple[Literal[1]]
reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None]
reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]]
reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None]
reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]]
reveal_type(t[4:]) # revealed: tuple[()]
reveal_type(t[:0]) # revealed: tuple[()]
reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]]
reveal_type(t[::2]) # revealed: tuple[Literal[1], None]
reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]]
reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]]
reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]]
reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]]
reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
start = 1
stop = None
step = 2
reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]]
reveal_type(t[False:True]) # revealed: tuple[Literal[1]]
reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None]
t[0:4:0] # error: [zero-stepsize-in-slice]
t[:4:0] # error: [zero-stepsize-in-slice]
t[0::0] # error: [zero-stepsize-in-slice]
t[::0] # error: [zero-stepsize-in-slice]
def _(m: int, n: int):
t = (1, "a", None, b"b")
reveal_type(t[0:0]) # revealed: tuple[()]
reveal_type(t[0:1]) # revealed: tuple[Literal[1]]
reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None]
reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]]
reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None]
reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]]
reveal_type(t[4:]) # revealed: tuple[()]
reveal_type(t[:0]) # revealed: tuple[()]
reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]]
reveal_type(t[::2]) # revealed: tuple[Literal[1], None]
reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]]
reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]]
reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]]
reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]]
reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]]
start = 1
stop = None
step = 2
reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]]
reveal_type(t[False:True]) # revealed: tuple[Literal[1]]
reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None]
t[0:4:0] # error: [zero-stepsize-in-slice]
t[:4:0] # error: [zero-stepsize-in-slice]
t[0::0] # error: [zero-stepsize-in-slice]
t[::0] # error: [zero-stepsize-in-slice]
tuple_slice = t[m:n]
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo(return type)

View File

@@ -117,9 +117,9 @@ properties on instance types:
```py path=b.py
import sys
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
```
## Accessing fields by index/slice

View File

@@ -33,7 +33,7 @@ in strict mode.
```py
def f(x: type):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(bound method)
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
class A: ...
@@ -48,7 +48,7 @@ x: type = A() # error: [invalid-assignment]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(bound method)
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
class A: ...

View File

@@ -263,12 +263,15 @@ static_assert(not is_assignable_to(int, Not[int]))
static_assert(not is_assignable_to(int, Not[Literal[1]]))
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Any, int], int))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
```

View File

@@ -1,310 +0,0 @@
# Disjointness relation
Two types `S` and `T` are disjoint if their intersection `S & T` is empty (equivalent to `Never`).
This means that it is known that no possible runtime object inhabits both types simultaneously.
## Basic builtin types
```py
from typing_extensions import Literal, LiteralString, Any
from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert
static_assert(is_disjoint_from(bool, str))
static_assert(not is_disjoint_from(bool, bool))
static_assert(not is_disjoint_from(bool, int))
static_assert(not is_disjoint_from(bool, object))
static_assert(not is_disjoint_from(Any, bool))
static_assert(not is_disjoint_from(Any, Any))
static_assert(not is_disjoint_from(LiteralString, LiteralString))
static_assert(not is_disjoint_from(str, LiteralString))
static_assert(not is_disjoint_from(str, type))
static_assert(not is_disjoint_from(str, type[Any]))
```
## Class hierarchies
```py
from knot_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of
from typing import final
class A: ...
class B1(A): ...
class B2(A): ...
# B1 and B2 are subclasses of A, so they are not disjoint from A:
static_assert(not is_disjoint_from(A, B1))
static_assert(not is_disjoint_from(A, B2))
# The two subclasses B1 and B2 are also not disjoint ...
static_assert(not is_disjoint_from(B1, B2))
# ... because they could share a common subclass ...
class C(B1, B2): ...
# ... which lies in their intersection:
static_assert(is_subtype_of(C, Intersection[B1, B2]))
# However, if a class is marked final, it can not be subclassed ...
@final
class FinalSubclass(A): ...
static_assert(not is_disjoint_from(FinalSubclass, A))
# ... which makes it disjoint from B1, B2:
static_assert(is_disjoint_from(B1, FinalSubclass))
static_assert(is_disjoint_from(B2, FinalSubclass))
```
## Tuple types
```py
from typing_extensions import Literal
from knot_extensions import TypeOf, is_disjoint_from, static_assert
static_assert(is_disjoint_from(tuple[()], TypeOf[object]))
static_assert(is_disjoint_from(tuple[()], TypeOf[Literal]))
static_assert(is_disjoint_from(tuple[None], None))
static_assert(is_disjoint_from(tuple[None], Literal[b"a"]))
static_assert(is_disjoint_from(tuple[None], Literal["a"]))
static_assert(is_disjoint_from(tuple[None], Literal[1]))
static_assert(is_disjoint_from(tuple[None], Literal[True]))
static_assert(is_disjoint_from(tuple[Literal[1]], tuple[Literal[2]]))
static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1]]))
static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[3]]))
static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], int]))
```
## Unions
```py
from typing_extensions import Literal
from knot_extensions import Intersection, is_disjoint_from, static_assert
static_assert(is_disjoint_from(Literal[1, 2], Literal[3]))
static_assert(is_disjoint_from(Literal[1, 2], Literal[3, 4]))
static_assert(not is_disjoint_from(Literal[1, 2], Literal[2]))
static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3]))
```
## Intersections
```py
from typing_extensions import Literal, final
from knot_extensions import Intersection, is_disjoint_from, static_assert
@final
class P: ...
@final
class Q: ...
@final
class R: ...
# For three pairwise disjoint classes ...
static_assert(is_disjoint_from(P, Q))
static_assert(is_disjoint_from(P, R))
static_assert(is_disjoint_from(Q, R))
# ... their intersections are also disjoint:
static_assert(is_disjoint_from(Intersection[P, Q], R))
static_assert(is_disjoint_from(Intersection[P, R], Q))
static_assert(is_disjoint_from(Intersection[Q, R], P))
# On the other hand, for non-disjoint classes ...
class X: ...
class Y: ...
class Z: ...
static_assert(not is_disjoint_from(X, Y))
static_assert(not is_disjoint_from(X, Z))
static_assert(not is_disjoint_from(Y, Z))
# ... their intersections are also not disjoint:
static_assert(not is_disjoint_from(Intersection[X, Y], Z))
static_assert(not is_disjoint_from(Intersection[X, Z], Y))
static_assert(not is_disjoint_from(Intersection[Y, Z], X))
```
## Special types
### `Never`
`Never` is disjoint from every type, including itself.
```py
from typing_extensions import Never
from knot_extensions import is_disjoint_from, static_assert
static_assert(is_disjoint_from(Never, Never))
static_assert(is_disjoint_from(Never, None))
static_assert(is_disjoint_from(Never, int))
static_assert(is_disjoint_from(Never, object))
```
### `None`
```py
from typing_extensions import Literal
from knot_extensions import is_disjoint_from, static_assert
static_assert(is_disjoint_from(None, Literal[True]))
static_assert(is_disjoint_from(None, Literal[1]))
static_assert(is_disjoint_from(None, Literal["test"]))
static_assert(is_disjoint_from(None, Literal[b"test"]))
static_assert(is_disjoint_from(None, LiteralString))
static_assert(is_disjoint_from(None, int))
static_assert(is_disjoint_from(None, type[object]))
static_assert(not is_disjoint_from(None, None))
static_assert(not is_disjoint_from(None, int | None))
static_assert(not is_disjoint_from(None, object))
```
### Literals
```py
from typing_extensions import Literal, LiteralString
from knot_extensions import TypeOf, is_disjoint_from, static_assert
static_assert(is_disjoint_from(Literal[True], Literal[False]))
static_assert(is_disjoint_from(Literal[True], Literal[1]))
static_assert(is_disjoint_from(Literal[False], Literal[0]))
static_assert(is_disjoint_from(Literal[1], Literal[2]))
static_assert(is_disjoint_from(Literal["a"], Literal["b"]))
static_assert(is_disjoint_from(Literal[b"a"], LiteralString))
static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"]))
static_assert(is_disjoint_from(Literal[b"a"], Literal["a"]))
static_assert(is_disjoint_from(type[object], TypeOf[Literal]))
static_assert(is_disjoint_from(type[str], LiteralString))
static_assert(not is_disjoint_from(Literal[True], Literal[True]))
static_assert(not is_disjoint_from(Literal[False], Literal[False]))
static_assert(not is_disjoint_from(Literal[True], bool))
static_assert(not is_disjoint_from(Literal[True], int))
static_assert(not is_disjoint_from(Literal[1], Literal[1]))
static_assert(not is_disjoint_from(Literal["a"], Literal["a"]))
static_assert(not is_disjoint_from(Literal["a"], LiteralString))
static_assert(not is_disjoint_from(Literal["a"], str))
```
### Class, module and function literals
```py
from types import ModuleType, FunctionType
from knot_extensions import TypeOf, is_disjoint_from, static_assert
class A: ...
class B: ...
type LiteralA = TypeOf[A]
type LiteralB = TypeOf[B]
# Class literals for different classes are always disjoint.
# They are singleton types that only contain the class object itself.
static_assert(is_disjoint_from(LiteralA, LiteralB))
# The class A is a subclass of A, so A is not disjoint from type[A]:
static_assert(not is_disjoint_from(LiteralA, type[A]))
# The class A is disjoint from type[B] because it's not a subclass of B:
static_assert(is_disjoint_from(LiteralA, type[B]))
# However, type[A] is not disjoint from type[B], as there could be
# classes that inherit from both A and B:
static_assert(not is_disjoint_from(type[A], type[B]))
import random
import math
static_assert(is_disjoint_from(TypeOf[random], TypeOf[math]))
static_assert(not is_disjoint_from(TypeOf[random], ModuleType))
static_assert(not is_disjoint_from(TypeOf[random], object))
def f(): ...
def g(): ...
static_assert(is_disjoint_from(TypeOf[f], TypeOf[g]))
static_assert(not is_disjoint_from(TypeOf[f], FunctionType))
static_assert(not is_disjoint_from(TypeOf[f], object))
```
### `AlwaysTruthy` and `AlwaysFalsy`
```py
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
static_assert(is_disjoint_from(None, AlwaysTruthy))
static_assert(not is_disjoint_from(None, AlwaysFalsy))
static_assert(is_disjoint_from(AlwaysFalsy, AlwaysTruthy))
static_assert(not is_disjoint_from(str, AlwaysFalsy))
static_assert(not is_disjoint_from(str, AlwaysTruthy))
static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy))
static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy))
```
### Instance types versus `type[T]` types
An instance type is disjoint from a `type[T]` type if the instance type is `@final` and the class of
the instance type is not a subclass of `T`'s metaclass.
```py
from typing import final
from knot_extensions import is_disjoint_from, static_assert
@final
class Foo: ...
static_assert(is_disjoint_from(Foo, type[int]))
static_assert(is_disjoint_from(type[object], Foo))
static_assert(is_disjoint_from(type[dict], Foo))
# Instance types can be disjoint from `type[]` types
# even if the instance type is a subtype of `type`
@final
class Meta1(type): ...
class UsesMeta1(metaclass=Meta1): ...
static_assert(not is_disjoint_from(Meta1, type[UsesMeta1]))
class Meta2(type): ...
class UsesMeta2(metaclass=Meta2): ...
static_assert(not is_disjoint_from(Meta2, type[UsesMeta2]))
static_assert(is_disjoint_from(Meta1, type[UsesMeta2]))
```
### `type[T]` versus `type[S]`
By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the
metaclass of `S`.
```py
from typing import final
from knot_extensions import static_assert, is_disjoint_from
@final
class Meta1(type): ...
class Meta2(type): ...
class UsesMeta1(metaclass=Meta1): ...
class UsesMeta2(metaclass=Meta2): ...
static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2]))
```

View File

@@ -32,56 +32,4 @@ static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2]))
static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2]))
```
## Differently ordered intersections and unions are equivalent
```py
from knot_extensions import is_equivalent_to, static_assert, Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
static_assert(is_equivalent_to(P | Q | R, P | R | Q)) # 1
static_assert(is_equivalent_to(P | Q | R, Q | P | R)) # 2
static_assert(is_equivalent_to(P | Q | R, Q | R | P)) # 3
static_assert(is_equivalent_to(P | Q | R, R | P | Q)) # 4
static_assert(is_equivalent_to(P | Q | R, R | Q | P)) # 5
static_assert(is_equivalent_to(P | R | Q, Q | P | R)) # 6
static_assert(is_equivalent_to(P | R | Q, Q | R | P)) # 7
static_assert(is_equivalent_to(P | R | Q, R | P | Q)) # 8
static_assert(is_equivalent_to(P | R | Q, R | Q | P)) # 9
static_assert(is_equivalent_to(Q | P | R, Q | R | P)) # 10
static_assert(is_equivalent_to(Q | P | R, R | P | Q)) # 11
static_assert(is_equivalent_to(Q | P | R, R | Q | P)) # 12
static_assert(is_equivalent_to(Q | R | P, R | P | Q)) # 13
static_assert(is_equivalent_to(Q | R | P, R | Q | P)) # 14
static_assert(is_equivalent_to(R | P | Q, R | Q | P)) # 15
static_assert(is_equivalent_to(str | None, None | str))
static_assert(is_equivalent_to(Intersection[P, Q], Intersection[Q, P]))
static_assert(is_equivalent_to(Intersection[Q, Not[P]], Intersection[Not[P], Q]))
static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P], R, Q]))
static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q]))
```
## Tuples containing equivalent but differently ordered unions/intersections are equivalent
```py
from knot_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not
from typing import Literal
class P: ...
class Q: ...
class R: ...
class S: ...
static_assert(is_equivalent_to(tuple[P | Q], tuple[Q | P]))
static_assert(is_equivalent_to(tuple[P | None], tuple[None | P]))
static_assert(
is_equivalent_to(tuple[Intersection[P, Q] | Intersection[R, Not[S]]], tuple[Intersection[Not[S], R] | Intersection[Q, P]])
)
```
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent

View File

@@ -1,54 +0,0 @@
# Fully-static types
A type is fully static iff it does not contain any gradual forms.
## Fully-static
```py
from typing_extensions import Literal, LiteralString, Never
from knot_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert
static_assert(is_fully_static(Never))
static_assert(is_fully_static(None))
static_assert(is_fully_static(Literal[1]))
static_assert(is_fully_static(Literal[True]))
static_assert(is_fully_static(Literal["abc"]))
static_assert(is_fully_static(Literal[b"abc"]))
static_assert(is_fully_static(LiteralString))
static_assert(is_fully_static(str))
static_assert(is_fully_static(object))
static_assert(is_fully_static(type))
static_assert(is_fully_static(TypeOf[str]))
static_assert(is_fully_static(TypeOf[Literal]))
static_assert(is_fully_static(str | None))
static_assert(is_fully_static(Intersection[str, Not[LiteralString]]))
static_assert(is_fully_static(tuple[()]))
static_assert(is_fully_static(tuple[int, object]))
static_assert(is_fully_static(type[str]))
static_assert(is_fully_static(type[object]))
```
## Non-fully-static
```py
from typing_extensions import Any, Literal, LiteralString
from knot_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert
static_assert(not is_fully_static(Any))
static_assert(not is_fully_static(Unknown))
static_assert(not is_fully_static(Any | str))
static_assert(not is_fully_static(str | Unknown))
static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]]))
static_assert(not is_fully_static(tuple[Any, ...]))
static_assert(not is_fully_static(tuple[int, Any]))
static_assert(not is_fully_static(type[Any]))
```

View File

@@ -1,64 +0,0 @@
# Gradual equivalence relation
Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also
materializations of `B`, and all materializations of `B` are also materializations of `A`.
## Basic
```py
from typing import Any
from typing_extensions import Literal, LiteralString, Never
from knot_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert
static_assert(is_gradual_equivalent_to(Any, Any))
static_assert(is_gradual_equivalent_to(Unknown, Unknown))
static_assert(is_gradual_equivalent_to(Any, Unknown))
static_assert(is_gradual_equivalent_to(Never, Never))
static_assert(is_gradual_equivalent_to(AlwaysTruthy, AlwaysTruthy))
static_assert(is_gradual_equivalent_to(AlwaysFalsy, AlwaysFalsy))
static_assert(is_gradual_equivalent_to(LiteralString, LiteralString))
static_assert(is_gradual_equivalent_to(Literal[True], Literal[True]))
static_assert(is_gradual_equivalent_to(Literal[False], Literal[False]))
static_assert(is_gradual_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2]))
static_assert(is_gradual_equivalent_to(TypeOf[str], TypeOf[str]))
static_assert(is_gradual_equivalent_to(type, type[object]))
static_assert(not is_gradual_equivalent_to(type, type[Any]))
static_assert(not is_gradual_equivalent_to(type[object], type[Any]))
```
## Unions and intersections
```py
from typing import Any
from knot_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert
static_assert(is_gradual_equivalent_to(str | int, str | int))
static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown))
static_assert(is_gradual_equivalent_to(str | int, int | str))
static_assert(
is_gradual_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]])
)
# TODO: `~type[Any]` shoudld be gradually equivalent to `~type[Unknown]`
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]]))
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
```
## Tuples
```py
from knot_extensions import Unknown, is_gradual_equivalent_to, static_assert
static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, bytes]))
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
```
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

View File

@@ -1,25 +0,0 @@
## Single-valued types
A type is single-valued iff it is not empty and all inhabitants of it compare equal.
```py
from typing_extensions import Any, Literal, LiteralString, Never
from knot_extensions import is_single_valued, static_assert
static_assert(is_single_valued(None))
static_assert(is_single_valued(Literal[True]))
static_assert(is_single_valued(Literal[1]))
static_assert(is_single_valued(Literal["abc"]))
static_assert(is_single_valued(Literal[b"abc"]))
static_assert(is_single_valued(tuple[()]))
static_assert(is_single_valued(tuple[Literal[True], Literal[1]]))
static_assert(not is_single_valued(str))
static_assert(not is_single_valued(Never))
static_assert(not is_single_valued(Any))
static_assert(not is_single_valued(Literal[1, 2]))
static_assert(not is_single_valued(tuple[None, int]))
```

View File

@@ -1,56 +0,0 @@
# Singleton types
A type is a singleton type iff it has exactly one inhabitant.
## Basic
```py
from typing_extensions import Literal, Never
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(None))
static_assert(is_singleton(Literal[True]))
static_assert(is_singleton(Literal[False]))
static_assert(is_singleton(type[bool]))
static_assert(not is_singleton(Never))
static_assert(not is_singleton(str))
static_assert(not is_singleton(Literal[345]))
static_assert(not is_singleton(Literal[1, 2]))
static_assert(not is_singleton(tuple[()]))
static_assert(not is_singleton(tuple[None]))
static_assert(not is_singleton(tuple[None, Literal[True]]))
```
## `NoDefault`
### 3.12
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import _NoDefaultType
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(_NoDefaultType))
```
### 3.13
```toml
[environment]
python-version = "3.13"
```
```py
from typing import _NoDefaultType
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(_NoDefaultType))
```

View File

@@ -1,27 +0,0 @@
# `__str__` and `__repr__`
```py
from typing_extensions import Literal, LiteralString
def _(
a: Literal[1],
b: Literal[True],
c: Literal[False],
d: Literal["ab'cd"],
e: LiteralString,
f: int,
):
reveal_type(str(a)) # revealed: Literal["1"]
reveal_type(str(b)) # revealed: Literal["True"]
reveal_type(str(c)) # revealed: Literal["False"]
reveal_type(str(d)) # revealed: Literal["ab'cd"]
reveal_type(str(e)) # revealed: LiteralString
reveal_type(str(f)) # revealed: str
reveal_type(repr(a)) # revealed: Literal["1"]
reveal_type(repr(b)) # revealed: Literal["True"]
reveal_type(repr(c)) # revealed: Literal["False"]
reveal_type(repr(d)) # revealed: Literal["'ab\\'cd'"]
reveal_type(repr(e)) # revealed: LiteralString
reveal_type(repr(f)) # revealed: str
```

View File

@@ -1,47 +0,0 @@
# Truthiness
```py
from typing_extensions import Literal, LiteralString
from knot_extensions import AlwaysFalsy, AlwaysTruthy
def _(
a: Literal[1],
b: Literal[-1],
c: Literal["foo"],
d: tuple[Literal[0]],
e: Literal[1, 2],
f: AlwaysTruthy,
):
reveal_type(bool(a)) # revealed: Literal[True]
reveal_type(bool(b)) # revealed: Literal[True]
reveal_type(bool(c)) # revealed: Literal[True]
reveal_type(bool(d)) # revealed: Literal[True]
reveal_type(bool(e)) # revealed: Literal[True]
reveal_type(bool(f)) # revealed: Literal[True]
def _(
a: tuple[()],
b: Literal[0],
c: Literal[""],
d: Literal[b""],
e: Literal[0, 0],
f: AlwaysFalsy,
):
reveal_type(bool(a)) # revealed: Literal[False]
reveal_type(bool(b)) # revealed: Literal[False]
reveal_type(bool(c)) # revealed: Literal[False]
reveal_type(bool(d)) # revealed: Literal[False]
reveal_type(bool(e)) # revealed: Literal[False]
reveal_type(bool(f)) # revealed: Literal[False]
def _(
a: str,
b: Literal[1, 0],
c: str | Literal[0],
d: str | Literal[1],
):
reveal_type(bool(a)) # revealed: bool
reveal_type(bool(b)) # revealed: bool
reveal_type(bool(c)) # revealed: bool
reveal_type(bool(d)) # revealed: bool
```

View File

@@ -1,92 +0,0 @@
# `typing.ClassVar`
[`typing.ClassVar`] is a type qualifier that is used to indicate that a class variable may not be
written to from instances of that class.
This test makes sure that we discover the type qualifier while inferring types from an annotation.
For more details on the semantics of pure class variables, see [this test](../attributes.md).
## Basic
```py
from typing import ClassVar, Annotated
class C:
a: ClassVar[int] = 1
b: Annotated[ClassVar[int], "the annotation for b"] = 1
c: ClassVar[Annotated[int, "the annotation for c"]] = 1
d: ClassVar = 1
e: "ClassVar[int]" = 1
reveal_type(C.a) # revealed: int
reveal_type(C.b) # revealed: int
reveal_type(C.c) # revealed: int
reveal_type(C.d) # revealed: Unknown | Literal[1]
reveal_type(C.e) # revealed: int
c = C()
# error: [invalid-attribute-access]
c.a = 2
# error: [invalid-attribute-access]
c.b = 2
# error: [invalid-attribute-access]
c.c = 2
# error: [invalid-attribute-access]
c.d = 2
# error: [invalid-attribute-access]
c.e = 2
```
## Conflicting type qualifiers
We currently ignore conflicting qualifiers and simply union them, which is more conservative than
intersecting them. This means that we consider `a` to be a `ClassVar` here:
```py
from typing import ClassVar
def flag() -> bool:
return True
class C:
if flag():
a: ClassVar[int] = 1
else:
a: str
reveal_type(C.a) # revealed: int | str
c = C()
# error: [invalid-attribute-access]
c.a = 2
```
## Too many arguments
```py
class C:
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` expects exactly one type parameter"
x: ClassVar[int, str] = 1
```
## Illegal `ClassVar` in type expression
```py
class C:
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
x: ClassVar | int
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
y: int | ClassVar[str]
```
## Used outside of a class
```py
# TODO: this should be an error
x: ClassVar[int] = 1
```
[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar

View File

@@ -139,9 +139,7 @@ reveal_type(not AlwaysFalse())
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
class BoolIsBool:
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
# https://github.com/astral-sh/ruff/issues/15672
__bool__: type[bool] = bool
__bool__ = bool
# revealed: bool
reveal_type(not BoolIsBool())

View File

@@ -76,11 +76,11 @@ with Manager():
```py
class Manager:
__enter__: int = 42
__enter__ = 42
def __exit__(self, exc_tpe, exc_value, traceback): ...
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `int` is not callable"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `Literal[42]` is not callable"
with Manager():
...
```
@@ -91,9 +91,9 @@ with Manager():
class Manager:
def __enter__(self) -> Self: ...
__exit__: int = 32
__exit__ = 32
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `Literal[32]` is not callable"
with Manager():
...
```

View File

@@ -156,6 +156,11 @@ pub(crate) mod tests {
self
}
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
self.custom_typeshed = Some(SystemPathBuf::from(path));
self
}
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
self.files.push((path, content));
self
@@ -170,8 +175,8 @@ pub(crate) mod tests {
db.write_files(self.files)
.context("Failed to write test files")?;
let mut search_paths = SearchPathSettings::new(vec![src_root]);
search_paths.custom_typeshed = self.custom_typeshed;
let mut search_paths = SearchPathSettings::new(src_root);
search_paths.typeshed = self.custom_typeshed;
Program::from_settings(
&db,

View File

@@ -10,7 +10,7 @@ pub use module_resolver::{resolve_module, system_module_search_paths, KnownModul
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
pub use python_platform::PythonPlatform;
pub use python_version::PythonVersion;
pub use semantic_model::{HasType, SemanticModel};
pub use semantic_model::{HasTy, SemanticModel};
pub mod ast_node_ref;
mod db;

View File

@@ -31,11 +31,6 @@ pub struct LintMetadata {
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
pub enum Level {
/// The lint is disabled and should not run.
Ignore,
@@ -409,12 +404,12 @@ impl From<&'static LintMetadata> for LintEntry {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default)]
pub struct RuleSelection {
/// Map with the severity for each enabled lint rule.
///
/// If a rule isn't present in this map, then it should be considered disabled.
lints: FxHashMap<LintId, (Severity, LintSource)>,
lints: FxHashMap<LintId, Severity>,
}
impl RuleSelection {
@@ -427,7 +422,7 @@ impl RuleSelection {
.filter_map(|lint| {
Severity::try_from(lint.default_level())
.ok()
.map(|severity| (*lint, (severity, LintSource::Default)))
.map(|severity| (*lint, severity))
})
.collect();
@@ -441,14 +436,12 @@ impl RuleSelection {
/// Returns an iterator over all enabled lints and their severity.
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + '_ {
self.lints
.iter()
.map(|(&lint, &(severity, _))| (lint, severity))
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
}
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
pub fn severity(&self, lint: LintId) -> Option<Severity> {
self.lints.get(&lint).map(|(severity, _)| *severity)
self.lints.get(&lint).copied()
}
/// Returns `true` if the `lint` is enabled.
@@ -459,25 +452,19 @@ impl RuleSelection {
/// Enables `lint` and configures with the given `severity`.
///
/// Overrides any previous configuration for the lint.
pub fn enable(&mut self, lint: LintId, severity: Severity, source: LintSource) {
self.lints.insert(lint, (severity, source));
pub fn enable(&mut self, lint: LintId, severity: Severity) {
self.lints.insert(lint, severity);
}
/// Disables `lint` if it was previously enabled.
pub fn disable(&mut self, lint: LintId) {
self.lints.remove(&lint);
}
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
pub enum LintSource {
/// The user didn't enable the rule explicitly, instead it's enabled by default.
#[default]
Default,
/// The rule was enabled by using a CLI argument
Cli,
/// The rule was enabled in a configuration file.
File,
/// Merges the enabled lints from `other` into this selection.
///
/// Lints from `other` will override any existing configuration.
pub fn merge(&mut self, other: &RuleSelection) {
self.lints.extend(other.iter());
}
}

View File

@@ -168,8 +168,8 @@ impl SearchPaths {
let SearchPathSettings {
extra_paths,
src_roots,
custom_typeshed: typeshed,
src_root,
typeshed,
site_packages: site_packages_paths,
} = settings;
@@ -186,10 +186,8 @@ impl SearchPaths {
static_paths.push(SearchPath::extra(system, path)?);
}
for src_root in src_roots {
tracing::debug!("Adding first-party search path '{src_root}'");
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
}
tracing::debug!("Adding first-party search path '{src_root}'");
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
let typeshed = canonicalize(typeshed, system);
@@ -222,14 +220,8 @@ impl SearchPaths {
static_paths.push(stdlib_path);
let site_packages_paths = match site_packages_paths {
SitePackages::Derived { venv_path } => {
// TODO: We may want to warn here if the venv's python version is older
// than the one resolved in the program settings because it indicates
// that the `target-version` is incorrectly configured or that the
// venv is out of date.
VirtualEnvironment::new(venv_path, system)
.and_then(|venv| venv.site_packages_directories(system))?
}
SitePackages::Derived { venv_path } => VirtualEnvironment::new(venv_path, system)
.and_then(|venv| venv.site_packages_directories(system))?,
SitePackages::Known(paths) => paths
.iter()
.map(|path| canonicalize(path, system))
@@ -1307,8 +1299,8 @@ mod tests {
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
src_roots: vec![src.clone()],
custom_typeshed: Some(custom_typeshed),
src_root: src.clone(),
typeshed: Some(custom_typeshed),
site_packages: SitePackages::Known(vec![site_packages]),
},
},
@@ -1813,8 +1805,8 @@ not_a_directory
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
src_roots: vec![SystemPathBuf::from("/src")],
custom_typeshed: None,
src_root: SystemPathBuf::from("/src"),
typeshed: None,
site_packages: SitePackages::Known(vec![
venv_site_packages,
system_site_packages,

View File

@@ -73,7 +73,7 @@ pub(crate) struct UnspecifiedTypeshed;
///
/// For tests checking that standard-library module resolution is working
/// correctly, you should usually create a [`MockedTypeshed`] instance
/// and pass it to the [`TestCaseBuilder::with_mocked_typeshed`] method.
/// and pass it to the [`TestCaseBuilder::with_custom_typeshed`] method.
/// If you need to check something that involves the vendored typeshed stubs
/// we include as part of the binary, you can instead use the
/// [`TestCaseBuilder::with_vendored_typeshed`] method.
@@ -237,8 +237,8 @@ impl TestCaseBuilder<MockedTypeshed> {
python_platform,
search_paths: SearchPathSettings {
extra_paths: vec![],
src_roots: vec![src.clone()],
custom_typeshed: Some(typeshed.clone()),
src_root: src.clone(),
typeshed: Some(typeshed.clone()),
site_packages: SitePackages::Known(vec![site_packages.clone()]),
},
},
@@ -295,7 +295,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
python_platform,
search_paths: SearchPathSettings {
site_packages: SitePackages::Known(vec![site_packages.clone()]),
..SearchPathSettings::new(vec![src.clone()])
..SearchPathSettings::new(src.clone())
},
},
)

View File

@@ -103,30 +103,30 @@ pub struct SearchPathSettings {
pub extra_paths: Vec<SystemPathBuf>,
/// The root of the project, used for finding first-party modules.
pub src_roots: Vec<SystemPathBuf>,
pub src_root: SystemPathBuf,
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
/// bundled as a zip file in the binary
pub custom_typeshed: Option<SystemPathBuf>,
pub typeshed: Option<SystemPathBuf>,
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
pub site_packages: SitePackages,
}
impl SearchPathSettings {
pub fn new(src_roots: Vec<SystemPathBuf>) -> Self {
pub fn new(src_root: SystemPathBuf) -> Self {
Self {
src_roots,
src_root,
extra_paths: vec![],
custom_typeshed: None,
typeshed: None,
site_packages: SitePackages::Known(vec![]),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum SitePackages {
Derived {
venv_path: SystemPathBuf,

View File

@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
use ruff_index::newtype_index;
use ruff_python_ast as ast;
use ruff_python_ast::ExprRef;
use ruff_python_ast::ExpressionRef;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::semantic_index;
@@ -60,12 +60,12 @@ pub trait HasScopedUseId {
impl HasScopedUseId for ast::ExprName {
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
let expression_ref = ExprRef::from(self);
let expression_ref = ExpressionRef::from(self);
expression_ref.scoped_use_id(db, scope)
}
}
impl HasScopedUseId for ast::ExprRef<'_> {
impl HasScopedUseId for ast::ExpressionRef<'_> {
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
let ast_ids = ast_ids(db, scope);
ast_ids.use_id(*self)
@@ -91,7 +91,7 @@ macro_rules! impl_has_scoped_expression_id {
($ty: ty) => {
impl HasScopedExpressionId for $ty {
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
let expression_ref = ExprRef::from(self);
let expression_ref = ExpressionRef::from(self);
expression_ref.scoped_expression_id(db, scope)
}
}
@@ -132,7 +132,7 @@ impl_has_scoped_expression_id!(ast::ExprSlice);
impl_has_scoped_expression_id!(ast::ExprIpyEscapeCommand);
impl_has_scoped_expression_id!(ast::Expr);
impl HasScopedExpressionId for ast::ExprRef<'_> {
impl HasScopedExpressionId for ast::ExpressionRef<'_> {
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
let ast_ids = ast_ids(db, scope);
ast_ids.expression_id(*self)
@@ -184,8 +184,8 @@ pub(crate) mod node_key {
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) struct ExpressionNodeKey(NodeKey);
impl From<ast::ExprRef<'_>> for ExpressionNodeKey {
fn from(value: ast::ExprRef<'_>) -> Self {
impl From<ast::ExpressionRef<'_>> for ExpressionNodeKey {
fn from(value: ast::ExpressionRef<'_>) -> Self {
Self(NodeKey::from_node(value))
}
}

View File

@@ -603,8 +603,8 @@ impl<'db> SemanticIndexBuilder<'db> {
let definition = self.add_definition(symbol, parameter);
// Insert a mapping from the inner Parameter node to the same definition. This
// ensures that calling `HasType::inferred_type` on the inner parameter returns
// Insert a mapping from the inner Parameter node to the same definition.
// This ensures that calling `HasTy::ty` on the inner parameter returns
// a valid type (and doesn't panic)
let existing_definition = self
.definitions_by_node

View File

@@ -1,14 +1,14 @@
use ruff_db::files::{File, FilePath};
use ruff_db::source::line_index;
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, ExprRef};
use ruff_python_ast::{Expr, ExpressionRef};
use ruff_source_file::LineIndex;
use crate::module_name::ModuleName;
use crate::module_resolver::{resolve_module, Module};
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::semantic_index;
use crate::types::{binding_type, infer_scope_types, Type};
use crate::types::{binding_ty, infer_scope_types, Type};
use crate::Db;
pub struct SemanticModel<'db> {
@@ -40,117 +40,117 @@ impl<'db> SemanticModel<'db> {
}
}
pub trait HasType {
pub trait HasTy {
/// Returns the inferred type of `self`.
///
/// ## Panics
/// May panic if `self` is from another file than `model`.
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>;
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>;
}
impl HasType for ast::ExprRef<'_> {
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
impl HasTy for ast::ExpressionRef<'_> {
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let index = semantic_index(model.db, model.file);
let file_scope = index.expression_scope_id(*self);
let scope = file_scope.to_scope_id(model.db, model.file);
let expression_id = self.scoped_expression_id(model.db, scope);
infer_scope_types(model.db, scope).expression_type(expression_id)
infer_scope_types(model.db, scope).expression_ty(expression_id)
}
}
macro_rules! impl_expression_has_type {
macro_rules! impl_expression_has_ty {
($ty: ty) => {
impl HasType for $ty {
impl HasTy for $ty {
#[inline]
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let expression_ref = ExprRef::from(self);
expression_ref.inferred_type(model)
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let expression_ref = ExpressionRef::from(self);
expression_ref.ty(model)
}
}
};
}
impl_expression_has_type!(ast::ExprBoolOp);
impl_expression_has_type!(ast::ExprNamed);
impl_expression_has_type!(ast::ExprBinOp);
impl_expression_has_type!(ast::ExprUnaryOp);
impl_expression_has_type!(ast::ExprLambda);
impl_expression_has_type!(ast::ExprIf);
impl_expression_has_type!(ast::ExprDict);
impl_expression_has_type!(ast::ExprSet);
impl_expression_has_type!(ast::ExprListComp);
impl_expression_has_type!(ast::ExprSetComp);
impl_expression_has_type!(ast::ExprDictComp);
impl_expression_has_type!(ast::ExprGenerator);
impl_expression_has_type!(ast::ExprAwait);
impl_expression_has_type!(ast::ExprYield);
impl_expression_has_type!(ast::ExprYieldFrom);
impl_expression_has_type!(ast::ExprCompare);
impl_expression_has_type!(ast::ExprCall);
impl_expression_has_type!(ast::ExprFString);
impl_expression_has_type!(ast::ExprStringLiteral);
impl_expression_has_type!(ast::ExprBytesLiteral);
impl_expression_has_type!(ast::ExprNumberLiteral);
impl_expression_has_type!(ast::ExprBooleanLiteral);
impl_expression_has_type!(ast::ExprNoneLiteral);
impl_expression_has_type!(ast::ExprEllipsisLiteral);
impl_expression_has_type!(ast::ExprAttribute);
impl_expression_has_type!(ast::ExprSubscript);
impl_expression_has_type!(ast::ExprStarred);
impl_expression_has_type!(ast::ExprName);
impl_expression_has_type!(ast::ExprList);
impl_expression_has_type!(ast::ExprTuple);
impl_expression_has_type!(ast::ExprSlice);
impl_expression_has_type!(ast::ExprIpyEscapeCommand);
impl_expression_has_ty!(ast::ExprBoolOp);
impl_expression_has_ty!(ast::ExprNamed);
impl_expression_has_ty!(ast::ExprBinOp);
impl_expression_has_ty!(ast::ExprUnaryOp);
impl_expression_has_ty!(ast::ExprLambda);
impl_expression_has_ty!(ast::ExprIf);
impl_expression_has_ty!(ast::ExprDict);
impl_expression_has_ty!(ast::ExprSet);
impl_expression_has_ty!(ast::ExprListComp);
impl_expression_has_ty!(ast::ExprSetComp);
impl_expression_has_ty!(ast::ExprDictComp);
impl_expression_has_ty!(ast::ExprGenerator);
impl_expression_has_ty!(ast::ExprAwait);
impl_expression_has_ty!(ast::ExprYield);
impl_expression_has_ty!(ast::ExprYieldFrom);
impl_expression_has_ty!(ast::ExprCompare);
impl_expression_has_ty!(ast::ExprCall);
impl_expression_has_ty!(ast::ExprFString);
impl_expression_has_ty!(ast::ExprStringLiteral);
impl_expression_has_ty!(ast::ExprBytesLiteral);
impl_expression_has_ty!(ast::ExprNumberLiteral);
impl_expression_has_ty!(ast::ExprBooleanLiteral);
impl_expression_has_ty!(ast::ExprNoneLiteral);
impl_expression_has_ty!(ast::ExprEllipsisLiteral);
impl_expression_has_ty!(ast::ExprAttribute);
impl_expression_has_ty!(ast::ExprSubscript);
impl_expression_has_ty!(ast::ExprStarred);
impl_expression_has_ty!(ast::ExprName);
impl_expression_has_ty!(ast::ExprList);
impl_expression_has_ty!(ast::ExprTuple);
impl_expression_has_ty!(ast::ExprSlice);
impl_expression_has_ty!(ast::ExprIpyEscapeCommand);
impl HasType for ast::Expr {
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
impl HasTy for ast::Expr {
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
match self {
Expr::BoolOp(inner) => inner.inferred_type(model),
Expr::Named(inner) => inner.inferred_type(model),
Expr::BinOp(inner) => inner.inferred_type(model),
Expr::UnaryOp(inner) => inner.inferred_type(model),
Expr::Lambda(inner) => inner.inferred_type(model),
Expr::If(inner) => inner.inferred_type(model),
Expr::Dict(inner) => inner.inferred_type(model),
Expr::Set(inner) => inner.inferred_type(model),
Expr::ListComp(inner) => inner.inferred_type(model),
Expr::SetComp(inner) => inner.inferred_type(model),
Expr::DictComp(inner) => inner.inferred_type(model),
Expr::Generator(inner) => inner.inferred_type(model),
Expr::Await(inner) => inner.inferred_type(model),
Expr::Yield(inner) => inner.inferred_type(model),
Expr::YieldFrom(inner) => inner.inferred_type(model),
Expr::Compare(inner) => inner.inferred_type(model),
Expr::Call(inner) => inner.inferred_type(model),
Expr::FString(inner) => inner.inferred_type(model),
Expr::StringLiteral(inner) => inner.inferred_type(model),
Expr::BytesLiteral(inner) => inner.inferred_type(model),
Expr::NumberLiteral(inner) => inner.inferred_type(model),
Expr::BooleanLiteral(inner) => inner.inferred_type(model),
Expr::NoneLiteral(inner) => inner.inferred_type(model),
Expr::EllipsisLiteral(inner) => inner.inferred_type(model),
Expr::Attribute(inner) => inner.inferred_type(model),
Expr::Subscript(inner) => inner.inferred_type(model),
Expr::Starred(inner) => inner.inferred_type(model),
Expr::Name(inner) => inner.inferred_type(model),
Expr::List(inner) => inner.inferred_type(model),
Expr::Tuple(inner) => inner.inferred_type(model),
Expr::Slice(inner) => inner.inferred_type(model),
Expr::IpyEscapeCommand(inner) => inner.inferred_type(model),
Expr::BoolOp(inner) => inner.ty(model),
Expr::Named(inner) => inner.ty(model),
Expr::BinOp(inner) => inner.ty(model),
Expr::UnaryOp(inner) => inner.ty(model),
Expr::Lambda(inner) => inner.ty(model),
Expr::If(inner) => inner.ty(model),
Expr::Dict(inner) => inner.ty(model),
Expr::Set(inner) => inner.ty(model),
Expr::ListComp(inner) => inner.ty(model),
Expr::SetComp(inner) => inner.ty(model),
Expr::DictComp(inner) => inner.ty(model),
Expr::Generator(inner) => inner.ty(model),
Expr::Await(inner) => inner.ty(model),
Expr::Yield(inner) => inner.ty(model),
Expr::YieldFrom(inner) => inner.ty(model),
Expr::Compare(inner) => inner.ty(model),
Expr::Call(inner) => inner.ty(model),
Expr::FString(inner) => inner.ty(model),
Expr::StringLiteral(inner) => inner.ty(model),
Expr::BytesLiteral(inner) => inner.ty(model),
Expr::NumberLiteral(inner) => inner.ty(model),
Expr::BooleanLiteral(inner) => inner.ty(model),
Expr::NoneLiteral(inner) => inner.ty(model),
Expr::EllipsisLiteral(inner) => inner.ty(model),
Expr::Attribute(inner) => inner.ty(model),
Expr::Subscript(inner) => inner.ty(model),
Expr::Starred(inner) => inner.ty(model),
Expr::Name(inner) => inner.ty(model),
Expr::List(inner) => inner.ty(model),
Expr::Tuple(inner) => inner.ty(model),
Expr::Slice(inner) => inner.ty(model),
Expr::IpyEscapeCommand(inner) => inner.ty(model),
}
}
}
macro_rules! impl_binding_has_ty {
($ty: ty) => {
impl HasType for $ty {
impl HasTy for $ty {
#[inline]
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let index = semantic_index(model.db, model.file);
let binding = index.definition(self);
binding_type(model.db, binding)
binding_ty(model.db, binding)
}
}
};
@@ -168,10 +168,10 @@ mod tests {
use ruff_db::parsed::parsed_module;
use crate::db::tests::TestDbBuilder;
use crate::{HasType, SemanticModel};
use crate::{HasTy, SemanticModel};
#[test]
fn function_type() -> anyhow::Result<()> {
fn function_ty() -> anyhow::Result<()> {
let db = TestDbBuilder::new()
.with_file("/src/foo.py", "def test(): pass")
.build()?;
@@ -182,7 +182,7 @@ mod tests {
let function = ast.suite()[0].as_function_def_stmt().unwrap();
let model = SemanticModel::new(&db, foo);
let ty = function.inferred_type(&model);
let ty = function.ty(&model);
assert!(ty.is_function_literal());
@@ -190,7 +190,7 @@ mod tests {
}
#[test]
fn class_type() -> anyhow::Result<()> {
fn class_ty() -> anyhow::Result<()> {
let db = TestDbBuilder::new()
.with_file("/src/foo.py", "class Test: pass")
.build()?;
@@ -201,7 +201,7 @@ mod tests {
let class = ast.suite()[0].as_class_def_stmt().unwrap();
let model = SemanticModel::new(&db, foo);
let ty = class.inferred_type(&model);
let ty = class.ty(&model);
assert!(ty.is_class_literal());
@@ -209,7 +209,7 @@ mod tests {
}
#[test]
fn alias_type() -> anyhow::Result<()> {
fn alias_ty() -> anyhow::Result<()> {
let db = TestDbBuilder::new()
.with_file("/src/foo.py", "class Test: pass")
.with_file("/src/bar.py", "from foo import Test")
@@ -222,7 +222,7 @@ mod tests {
let import = ast.suite()[0].as_import_from_stmt().unwrap();
let alias = &import.names[0];
let model = SemanticModel::new(&db, bar);
let ty = alias.inferred_type(&model);
let ty = alias.ty(&model);
assert!(ty.is_class_literal());

View File

@@ -85,14 +85,6 @@ impl<'db> Symbol<'db> {
Symbol::Unbound => self,
}
}
#[must_use]
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> {
match self {
Symbol::Type(ty, boundness) => Symbol::Type(f(ty), boundness),
Symbol::Unbound => Symbol::Unbound,
}
}
}
#[cfg(test)]

File diff suppressed because it is too large Load Diff

View File

@@ -89,13 +89,13 @@ impl<'db> CallOutcome<'db> {
}
/// Get the return type of the call, or `None` if not callable.
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Callable { binding } => Some(binding.return_type()),
Self::Callable { binding } => Some(binding.return_ty()),
Self::RevealType {
binding,
revealed_ty: _,
} => Some(binding.return_type()),
} => Some(binding.return_ty()),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
@@ -105,7 +105,7 @@ impl<'db> CallOutcome<'db> {
// If all outcomes are NotCallable, we return None; if some outcomes are callable
// and some are not, we return a union including Unknown.
.fold(None, |acc, outcome| {
let ty = outcome.return_type(db);
let ty = outcome.return_ty(db);
match (acc, ty) {
(None, None) => None,
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
@@ -113,12 +113,12 @@ impl<'db> CallOutcome<'db> {
}
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_type(db),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
Self::StaticAssertionError { .. } => Some(Type::none(db)),
Self::AssertType {
binding,
asserted_ty: _,
} => Some(binding.return_type()),
} => Some(binding.return_ty()),
}
}
@@ -128,7 +128,7 @@ impl<'db> CallOutcome<'db> {
context: &InferContext<'db>,
node: ast::AnyNodeRef,
) -> Type<'db> {
match self.return_type_result(context, node) {
match self.return_ty_result(context, node) {
Ok(return_ty) => return_ty,
Err(NotCallableError::Type {
not_callable_ty,
@@ -194,7 +194,7 @@ impl<'db> CallOutcome<'db> {
}
/// Get the return type of the call as a result.
pub(super) fn return_type_result(
pub(super) fn return_ty_result(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
@@ -205,7 +205,7 @@ impl<'db> CallOutcome<'db> {
match self {
Self::Callable { binding } => {
binding.report_diagnostics(context, node);
Ok(binding.return_type())
Ok(binding.return_ty())
}
Self::RevealType {
binding,
@@ -218,7 +218,7 @@ impl<'db> CallOutcome<'db> {
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
);
Ok(binding.return_type())
Ok(binding.return_ty())
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
@@ -230,7 +230,7 @@ impl<'db> CallOutcome<'db> {
} => Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: *called_ty,
return_ty: call_outcome
.return_type(context.db())
.return_ty(context.db())
.unwrap_or(Type::unknown()),
}),
Self::Union {
@@ -251,7 +251,7 @@ impl<'db> CallOutcome<'db> {
revealed_ty: _,
} => {
if revealed {
binding.return_type()
binding.return_ty()
} else {
revealed = true;
outcome.unwrap_with_diagnostic(context, node)
@@ -329,8 +329,8 @@ impl<'db> CallOutcome<'db> {
binding,
asserted_ty,
} => {
let [actual_ty, _asserted] = binding.parameter_types() else {
return Ok(binding.return_type());
let [actual_ty, _asserted] = binding.parameter_tys() else {
return Ok(binding.return_ty());
};
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
@@ -345,7 +345,7 @@ impl<'db> CallOutcome<'db> {
);
}
Ok(binding.return_type())
Ok(binding.return_ty())
}
}
}
@@ -358,9 +358,9 @@ pub(super) enum CallDunderResult<'db> {
}
impl<'db> CallDunderResult<'db> {
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::CallOutcome(outcome) => outcome.return_type(db),
Self::CallOutcome(outcome) => outcome.return_ty(db),
Self::PossiblyUnbound { .. } => None,
Self::MethodNotAvailable => None,
}
@@ -394,7 +394,7 @@ pub(super) enum NotCallableError<'db> {
impl<'db> NotCallableError<'db> {
/// The return type that should be used when a call is not callable.
pub(super) fn return_type(&self) -> Type<'db> {
pub(super) fn return_ty(&self) -> Type<'db> {
match self {
Self::Type { return_ty, .. } => *return_ty,
Self::UnionElement { return_ty, .. } => *return_ty,
@@ -407,7 +407,7 @@ impl<'db> NotCallableError<'db> {
///
/// For unions, returns the union type itself, which may contain a mix of callable and
/// non-callable types.
pub(super) fn called_type(&self) -> Type<'db> {
pub(super) fn called_ty(&self) -> Type<'db> {
match self {
Self::Type {
not_callable_ty, ..

View File

@@ -73,7 +73,7 @@ pub(crate) fn bind_call<'db>(
continue;
}
};
if let Some(expected_ty) = parameter.annotated_type() {
if let Some(expected_ty) = parameter.annotated_ty() {
if !argument_ty.is_assignable_to(db, expected_ty) {
errors.push(CallBindingError::InvalidArgumentType {
parameter: ParameterContext::new(parameter, index, positional),
@@ -109,8 +109,7 @@ pub(crate) fn bind_call<'db>(
for (index, bound_ty) in parameter_tys.iter().enumerate() {
if bound_ty.is_none() {
let param = &parameters[index];
if param.is_variadic() || param.is_keyword_variadic() || param.default_type().is_some()
{
if param.is_variadic() || param.is_keyword_variadic() || param.default_ty().is_some() {
// variadic/keywords and defaulted arguments are not required
continue;
}
@@ -152,7 +151,7 @@ pub(crate) struct CallBinding<'db> {
impl<'db> CallBinding<'db> {
// TODO remove this constructor and construct always from `bind_call`
pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self {
pub(crate) fn from_return_ty(return_ty: Type<'db>) -> Self {
Self {
callable_ty: None,
return_ty,
@@ -161,27 +160,27 @@ impl<'db> CallBinding<'db> {
}
}
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
pub(crate) fn set_return_ty(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
}
pub(crate) fn return_type(&self) -> Type<'db> {
pub(crate) fn return_ty(&self) -> Type<'db> {
self.return_ty
}
pub(crate) fn parameter_types(&self) -> &[Type<'db>] {
pub(crate) fn parameter_tys(&self) -> &[Type<'db>] {
&self.parameter_tys
}
pub(crate) fn one_parameter_type(&self) -> Option<Type<'db>> {
match self.parameter_types() {
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
match self.parameter_tys() {
[ty] => Some(*ty),
_ => None,
}
}
pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> {
match self.parameter_types() {
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
match self.parameter_tys() {
[first, second] => Some((*first, *second)),
_ => None,
}

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