Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie Blue
70cf8a94d5 Disable CRL checks during Windows test CI 2025-02-05 15:24:44 -06:00
1090 changed files with 11598 additions and 28286 deletions

View File

@@ -58,12 +58,6 @@
description: "Disable PRs updating GitHub runners (e.g. 'runs-on: macos-14')",
enabled: false,
},
{
// TODO: Remove this once the codebase is upgrade to v4 (https://github.com/astral-sh/ruff/pull/16069)
matchPackageNames: ["tailwindcss"],
matchManagers: ["npm"],
enabled: false,
},
{
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
// See: https://github.com/astral-sh/uv/issues/3642

View File

@@ -217,6 +217,11 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
# There are spurious CRL server offline errors when downloading
# `cargo-bloat` with curl below, so we just disable them for now
- name: "Disable SChannel CRL checks"
run: |
reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v EnableCRLCheck /t REG_DWORD /d 0 /f
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:
@@ -712,7 +717,7 @@ jobs:
just test
benchmarks:
runs-on: ubuntu-24.04
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') }}
timeout-minutes: 20

View File

@@ -35,8 +35,6 @@ jobs:
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@v0.2.0
- name: "Run wasm-pack"
run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff_wasm
@@ -51,7 +49,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.14.0
uses: cloudflare/wrangler-action@v3.13.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -35,8 +35,6 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@v0.2.0
- name: "Run wasm-pack build"
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm

View File

@@ -60,7 +60,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.29.7
rev: v1.29.5
hooks:
- id: typos
@@ -74,7 +74,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
rev: v0.9.4
hooks:
- id: ruff-format
- id: ruff
@@ -84,7 +84,7 @@ repos:
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.5.1
rev: v3.4.2
hooks:
- id: prettier
types: [yaml]
@@ -92,7 +92,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.3.1
rev: v1.3.0
hooks:
- id: zizmor

View File

@@ -209,8 +209,8 @@ This change only affects those using Ruff under its default rule set. Users that
### Remove support for emoji identifiers ([#7212](https://github.com/astral-sh/ruff/pull/7212))
Previously, Ruff supported non-standards-compliant emoji identifiers such as `📦 = 1`.
We decided to remove this non-standard language extension. Ruff now reports syntax errors for invalid emoji identifiers in your code, the same as CPython.
Previously, Ruff supported the non-standard compliant emoji identifiers e.g. `📦 = 1`.
We decided to remove this non-standard language extension, and Ruff now reports syntax errors for emoji identifiers in your code, the same as CPython.
### Improved GitLab fingerprints ([#7203](https://github.com/astral-sh/ruff/pull/7203))

View File

@@ -1,153 +1,5 @@
# Changelog
## 0.9.7
### Preview features
- Consider `__new__` methods as special function type for enforcing class method or static method rules ([#13305](https://github.com/astral-sh/ruff/pull/13305))
- \[`airflow`\] Improve the internal logic to differentiate deprecated symbols (`AIR303`) ([#16013](https://github.com/astral-sh/ruff/pull/16013))
- \[`refurb`\] Manual timezone monkeypatching (`FURB162`) ([#16113](https://github.com/astral-sh/ruff/pull/16113))
- \[`ruff`\] Implicit class variable in dataclass (`RUF045`) ([#14349](https://github.com/astral-sh/ruff/pull/14349))
- \[`ruff`\] Skip singleton starred expressions for `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#16083](https://github.com/astral-sh/ruff/pull/16083))
- \[`refurb`\] Check for subclasses includes subscript expressions (`FURB189`) ([#16155](https://github.com/astral-sh/ruff/pull/16155))
### Rule changes
- \[`flake8-comprehensions`\]: Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110))
- \[`flake8-debugger`\] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#16191](https://github.com/astral-sh/ruff/pull/16191))
- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011))
- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080))
- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132))
- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219))
- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091))
- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919))
- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122))
- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141))
- \[`pycodestyle`\] Exempt `site.addsitedir(...)` calls (`E402`) ([#16251](https://github.com/astral-sh/ruff/pull/16251))
### Formatter
- Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values ([#16187](https://github.com/astral-sh/ruff/pull/16187))
### Server
- Fix handling of requests received after shutdown message ([#16262](https://github.com/astral-sh/ruff/pull/16262))
- Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code actions for a notebook cell ([#16154](https://github.com/astral-sh/ruff/pull/16154))
- Include document specific debug info for `ruff.printDebugInformation` ([#16215](https://github.com/astral-sh/ruff/pull/16215))
- Update server to return the debug info as string with `ruff.printDebugInformation` ([#16214](https://github.com/astral-sh/ruff/pull/16214))
### CLI
- Warn on invalid `noqa` even when there are no diagnostics ([#16178](https://github.com/astral-sh/ruff/pull/16178))
- Better error messages while loading configuration `extend`s ([#15658](https://github.com/astral-sh/ruff/pull/15658))
### Bug fixes
- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237))
### Documentation
- Add FAQ entry for `source.*` code actions in Notebook ([#16212](https://github.com/astral-sh/ruff/pull/16212))
- Add `SECURITY.md` ([#16224](https://github.com/astral-sh/ruff/pull/16224))
## 0.9.6
### Preview features
- \[`airflow`\] Add `external_task.{ExternalTaskMarker, ExternalTaskSensor}` for `AIR302` ([#16014](https://github.com/astral-sh/ruff/pull/16014))
- \[`flake8-builtins`\] Make strict module name comparison optional (`A005`) ([#15951](https://github.com/astral-sh/ruff/pull/15951))
- \[`flake8-pyi`\] Extend fix to Python \<= 3.9 for `redundant-none-literal` (`PYI061`) ([#16044](https://github.com/astral-sh/ruff/pull/16044))
- \[`pylint`\] Also report when the object isn't a literal (`PLE1310`) ([#15985](https://github.com/astral-sh/ruff/pull/15985))
- \[`ruff`\] Implement `indented-form-feed` (`RUF054`) ([#16049](https://github.com/astral-sh/ruff/pull/16049))
- \[`ruff`\] Skip type definitions for `missing-f-string-syntax` (`RUF027`) ([#16054](https://github.com/astral-sh/ruff/pull/16054))
### Rule changes
- \[`flake8-annotations`\] Correct syntax for `typing.Union` in suggested return type fixes for `ANN20x` rules ([#16025](https://github.com/astral-sh/ruff/pull/16025))
- \[`flake8-builtins`\] Match upstream module name comparison (`A005`) ([#16006](https://github.com/astral-sh/ruff/pull/16006))
- \[`flake8-comprehensions`\] Detect overshadowed `list`/`set`/`dict`, ignore variadics and named expressions (`C417`) ([#15955](https://github.com/astral-sh/ruff/pull/15955))
- \[`flake8-pie`\] Remove following comma correctly when the unpacked dictionary is empty (`PIE800`) ([#16008](https://github.com/astral-sh/ruff/pull/16008))
- \[`flake8-simplify`\] Only trigger `SIM401` on known dictionaries ([#15995](https://github.com/astral-sh/ruff/pull/15995))
- \[`pylint`\] Do not report calls when object type and argument type mismatch, remove custom escape handling logic (`PLE1310`) ([#15984](https://github.com/astral-sh/ruff/pull/15984))
- \[`pyupgrade`\] Comments within parenthesized value ranges should not affect applicability (`UP040`) ([#16027](https://github.com/astral-sh/ruff/pull/16027))
- \[`pyupgrade`\] Don't introduce invalid syntax when upgrading old-style type aliases with parenthesized multiline values (`UP040`) ([#16026](https://github.com/astral-sh/ruff/pull/16026))
- \[`pyupgrade`\] Ensure we do not rename two type parameters to the same name (`UP049`) ([#16038](https://github.com/astral-sh/ruff/pull/16038))
- \[`pyupgrade`\] \[`ruff`\] Don't apply renamings if the new name is shadowed in a scope of one of the references to the binding (`UP049`, `RUF052`) ([#16032](https://github.com/astral-sh/ruff/pull/16032))
- \[`ruff`\] Update `RUF009` to behave similar to `B008` and ignore attributes with immutable types ([#16048](https://github.com/astral-sh/ruff/pull/16048))
### Server
- Root exclusions in the server to project root ([#16043](https://github.com/astral-sh/ruff/pull/16043))
### Bug fixes
- \[`flake8-datetime`\] Ignore `.replace()` calls while looking for `.astimezone` ([#16050](https://github.com/astral-sh/ruff/pull/16050))
- \[`flake8-type-checking`\] Avoid `TC004` false positive where the runtime definition is provided by `__getattr__` ([#16052](https://github.com/astral-sh/ruff/pull/16052))
### Documentation
- Improve `ruff-lsp` migration document ([#16072](https://github.com/astral-sh/ruff/pull/16072))
- Undeprecate `ruff.nativeServer` ([#16039](https://github.com/astral-sh/ruff/pull/16039))
## 0.9.5
### Preview features
- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719))
- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837))
- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799))
- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853))
- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821))
- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889))
- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854))
- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762))
- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872))
- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862))
- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905))
- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832))
- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841))
- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922))
- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999))
- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888))
### Rule changes
- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818))
- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838))
- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885))
- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954))
- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840))
- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921))
- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980))
### Configuration
- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918))
- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992))
### Bug fixes
- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877))
- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929))
- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851))
- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988))
- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829))
- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930))
- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953))
- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779))
### Documentation
- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850))
- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882))
- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946))
- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909))
- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844))
- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928))
- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956))
- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982))
- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883))
## 0.9.4
### Preview features

View File

@@ -526,7 +526,7 @@ cargo benchmark
#### Benchmark-driven Development
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g., on `main`) and then use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g. on `main`) and then use
`--benchmark=<name>` to compare against that benchmark. Criterion will print a message telling you
if the benchmark improved/regressed compared to that baseline.
@@ -678,9 +678,9 @@ utils with it:
23 Newline 24
```
- `cargo dev print-cst <file>`: Print the CST of a Python file using
- `cargo dev print-cst <file>`: Print the CST of a python file using
[LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser
in Ruff. For example, for `if True: pass # comment`, everything, including the whitespace, is represented:
in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented:
```text
Module {

138
Cargo.lock generated
View File

@@ -29,12 +29,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -360,9 +354,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.29"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
dependencies = [
"clap_builder",
"clap_derive",
@@ -370,9 +364,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.29"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
dependencies = [
"anstream",
"anstyle",
@@ -413,9 +407,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.28"
version = "4.5.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
dependencies = [
"heck",
"proc-macro2",
@@ -444,22 +438,20 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.8.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25d2f5a6570db487f5258e0bded6352fa2034c2aeb46bb5cc3ff060a0fcfba2f"
checksum = "450a0e9df9df1c154156f4344f99d8f6f6e69d0fc4de96ef6e2e68b2ec3bce97"
dependencies = [
"colored 2.2.0",
"libc",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "codspeed-criterion-compat"
version = "2.8.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f53a55558dedec742b14aae3c5fec389361b8b5ca28c1aadf09dd91faf710074"
checksum = "8eb1a6cb9c20e177fde58cdef97c1c7c9264eb1424fe45c4fccedc2fb078a569"
dependencies = [
"codspeed",
"colored 2.2.0",
@@ -905,7 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1039,8 +1031,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1104,7 +1098,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
@@ -1482,7 +1475,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
dependencies = [
"hermit-abi 0.4.0",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2056,7 +2049,7 @@ dependencies = [
"once_cell",
"pep440_rs",
"regex",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"smallvec",
"thiserror 1.0.69",
@@ -2416,7 +2409,6 @@ dependencies = [
"red_knot_server",
"regex",
"ruff_db",
"ruff_python_ast",
"ruff_python_trivia",
"salsa",
"tempfile",
@@ -2445,9 +2437,8 @@ dependencies = [
"ruff_macros",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
"thiserror 2.0.11",
"toml",
@@ -2485,9 +2476,8 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
"smallvec",
"static_assertions",
@@ -2513,7 +2503,7 @@ dependencies = [
"ruff_python_ast",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"shellexpand",
@@ -2535,11 +2525,10 @@ dependencies = [
"regex",
"ruff_db",
"ruff_index",
"ruff_python_ast",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"serde",
"smallvec",
@@ -2566,9 +2555,9 @@ dependencies = [
"js-sys",
"log",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -2650,7 +2639,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.7"
version = "0.9.4"
dependencies = [
"anyhow",
"argfile",
@@ -2690,7 +2679,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"shellexpand",
@@ -2729,13 +2718,14 @@ dependencies = [
"mimalloc",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_linter",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"tikv-jemallocator",
]
@@ -2757,10 +2747,10 @@ name = "ruff_db"
version = "0.0.0"
dependencies = [
"camino",
"colored 3.0.0",
"countme",
"dashmap 6.1.0",
"dunce",
"etcetera",
"filetime",
"glob",
"ignore",
@@ -2775,9 +2765,8 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
"tempfile",
"thiserror 2.0.11",
@@ -2802,7 +2791,6 @@ dependencies = [
"libcst",
"pretty_assertions",
"rayon",
"red_knot_project",
"regex",
"ruff",
"ruff_diagnostics",
@@ -2846,7 +2834,7 @@ dependencies = [
"ruff_cache",
"ruff_macros",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"static_assertions",
@@ -2878,13 +2866,12 @@ name = "ruff_index"
version = "0.0.0"
dependencies = [
"ruff_macros",
"salsa",
"static_assertions",
]
[[package]]
name = "ruff_linter"
version = "0.9.7"
version = "0.9.4"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2926,7 +2913,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"serde_json",
@@ -2988,8 +2975,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"salsa",
"rustc-hash 2.1.0",
"schemars",
"serde",
]
@@ -3036,7 +3022,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"serde_json",
@@ -3083,7 +3069,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"static_assertions",
"unicode-ident",
"unicode-normalization",
@@ -3114,7 +3100,7 @@ dependencies = [
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"smallvec",
@@ -3173,7 +3159,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"shellexpand",
@@ -3203,7 +3189,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.7"
version = "0.9.4"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3237,7 +3223,6 @@ dependencies = [
"glob",
"globset",
"ignore",
"indexmap",
"is-macro",
"itertools 0.14.0",
"log",
@@ -3256,7 +3241,7 @@ dependencies = [
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"shellexpand",
@@ -3283,9 +3268,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustix"
@@ -3297,7 +3282,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3315,19 +3300,17 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
dependencies = [
"append-only-vec",
"arc-swap",
"compact_str",
"crossbeam",
"dashmap 6.1.0",
"hashbrown 0.14.5",
"hashlink",
"indexmap",
"parking_lot",
"rayon",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa-macro-rules",
"salsa-macros",
"smallvec",
@@ -3337,12 +3320,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
dependencies = [
"heck",
"proc-macro2",
@@ -3551,9 +3534,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "smallvec"
version = "1.14.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "snapbox"
@@ -3613,18 +3596,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.1"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
@@ -3668,16 +3651,16 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.17.0"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3866,9 +3849,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.20"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
@@ -4165,22 +4148,21 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.13.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom 0.3.1",
"js-sys",
"rand 0.9.0",
"getrandom 0.2.15",
"rand 0.8.5",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.13.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d28dd23acb5f2fa7bd2155ab70b960e770596b3bb6395119b40476c3655dfba4"
checksum = "f8a86d88347b61a0e17b9908a67efcc594130830bf1045653784358dd023e294"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -143,8 +143,8 @@ snapbox = { version = "0.6.0", features = [
"examples",
] }
static_assertions = "1.1.0"
strum = { version = "0.27.0", features = ["strum_macros"] }
strum_macros = { version = "0.27.0" }
strum = { version = "0.26.0", features = ["strum_macros"] }
strum_macros = { version = "0.26.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.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.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.4/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.7
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff
@@ -452,7 +452,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
- [Ibis](https://github.com/ibis-project/ibis)
- [ivy](https://github.com/unifyai/ivy)
- [JAX](https://github.com/jax-ml/jax)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Kraken Tech](https://kraken.tech/)
- [LangChain](https://github.com/hwchase17/langchain)

View File

@@ -1,15 +0,0 @@
# Security policy
## Reporting a vulnerability
If you have found a possible vulnerability, please email `security at astral dot sh`.
## Bug bounties
While we sincerely appreciate and encourage reports of suspected security problems, please note that
Astral does not currently run any bug bounty programs.
## Vulnerability disclosures
Critical vulnerabilities will be disclosed via GitHub's
[security advisory](https://github.com/astral-sh/ruff/security) system.

View File

@@ -16,7 +16,6 @@ red_knot_python_semantic = { workspace = true }
red_knot_project = { workspace = true, features = ["zstd"] }
red_knot_server = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache"] }
ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }

View File

@@ -1,25 +0,0 @@
# Red Knot
Red Knot is an extremely fast type checker.
Currently, it is a work-in-progress and not ready for user testing.
Red Knot is designed to prioritize good type inference, even in unannotated code,
and aims to avoid false positives.
While Red Knot will produce similar results to mypy and pyright on many codebases,
100% compatibility with these tools is a non-goal.
On some codebases, Red Knot's design decisions lead to different outcomes
than you would get from running one of these more established tools.
## Contributing
Core type checking tests are written as Markdown code blocks.
They can be found in [`red_knot_python_semantic/resources/mdtest`][resources-mdtest].
See [`red_knot_test/README.md`][mdtest-readme] for more information
on the test framework itself.
The list of open issues can be found [here][open-issues].
[mdtest-readme]: ../red_knot_test/README.md
[open-issues]: https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Ared-knot
[resources-mdtest]: ../red_knot_python_semantic/resources/mdtest

View File

@@ -1,7 +1,7 @@
use crate::logging::Verbosity;
use crate::python_version::PythonVersion;
use clap::{ArgAction, ArgMatches, Error, Parser};
use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_python_semantic::lint;
use ruff_db::system::SystemPathBuf;
@@ -67,8 +67,8 @@ pub(crate) struct CheckCommand {
pub(crate) rules: RulesArg,
/// Use exit code 1 if there are any warning-level diagnostics.
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
pub(crate) error_on_warning: Option<bool>,
#[arg(long, conflicts_with = "exit_zero")]
pub(crate) error_on_warning: bool,
/// Always use exit code 0, even when there are error-level diagnostics.
#[arg(long)]
@@ -107,9 +107,6 @@ impl CheckCommand {
}),
..EnvironmentOptions::default()
}),
terminal: Some(TerminalOptions {
error_on_warning: self.error_on_warning,
}),
rules,
..Default::default()
}

View File

@@ -11,17 +11,18 @@ use clap::Parser;
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use red_knot_project::metadata::options::Options;
use red_knot_project::watch;
use red_knot_project::watch::ProjectWatcher;
use red_knot_project::{watch, Db};
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_server::run_server;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
mod args;
mod logging;
mod python_version;
mod verbosity;
mod version;
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
@@ -96,15 +97,19 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let min_error_severity = if args.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let cli_options = args.into_options();
let mut project_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
project_metadata.apply_cli_options(cli_options.clone());
project_metadata.apply_configuration_files(&system)?;
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
workspace_metadata.apply_cli_options(cli_options.clone());
let mut db = ProjectDatabase::new(project_metadata, system)?;
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_options, min_error_severity);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -162,10 +167,18 @@ struct MainLoop {
watcher: Option<ProjectWatcher>,
cli_options: Options,
/// The minimum severity to consider an error when deciding the exit status.
///
/// TODO(micha): Get from the terminal settings.
min_error_severity: Severity,
}
impl MainLoop {
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
fn new(
cli_options: Options,
min_error_severity: Severity,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
(
@@ -174,6 +187,7 @@ impl MainLoop {
receiver,
watcher: None,
cli_options,
min_error_severity,
},
MainLoopCancellationToken { sender },
)
@@ -231,24 +245,14 @@ impl MainLoop {
result,
revision: check_revision,
} => {
let display_config = DisplayDiagnosticConfig::default()
.color(colored::control::SHOULD_COLORIZE.should_colorize());
let min_error_severity =
if db.project().settings(db).terminal().error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let failed = result
.iter()
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);
if check_revision == revision {
#[allow(clippy::print_stdout)]
for diagnostic in result {
println!("{}", diagnostic.display(db, &display_config));
println!("{}", diagnostic.display(db));
}
} else {
tracing::debug!(

View File

@@ -40,7 +40,7 @@ impl std::fmt::Display for PythonVersion {
}
}
impl From<PythonVersion> for ruff_python_ast::PythonVersion {
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
fn from(value: PythonVersion) -> Self {
match value {
PythonVersion::Py37 => Self::PY37,
@@ -61,8 +61,8 @@ mod tests {
#[test]
fn same_default_as_python_version() {
assert_eq!(
ruff_python_ast::PythonVersion::from(PythonVersion::default()),
ruff_python_ast::PythonVersion::default()
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
red_knot_python_semantic::PythonVersion::default()
);
}
}

View File

@@ -0,0 +1 @@

View File

@@ -98,7 +98,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
])?;
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r###"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -115,7 +115,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
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 -----
@@ -167,7 +167,7 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re
),
])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
success: true
exit_code: 0
----- stdout -----
@@ -575,37 +575,6 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
let case = TestCase::with_files([
("test.py", r"print(x) # [unresolved-reference]"),
(
"knot.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
let case = TestCase::with_file(
@@ -717,109 +686,6 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn user_configuration() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"project/knot.toml",
r#"
[rules]
division-by-zero = "warn"
"#,
),
(
"project/main.py",
r#"
y = 4 / 0
for a in range(0, y):
x = a
print(x)
"#,
),
])?;
let config_directory = case.root().join("home/.config");
let config_env_var = if cfg!(windows) {
"APPDATA"
} else {
"XDG_CONFIG_HOME"
};
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| - Name `x` used when possibly not defined
|
----- stderr -----
"###
);
// The user-level configuration promotes `possibly-unresolved-reference` to an error.
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
// has higher precedence.
case.write_file(
config_directory.join("knot/knot.toml"),
r#"
[rules]
division-by-zero = "error"
possibly-unresolved-reference = "error"
"#,
)?;
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
error: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
|
----- stderr -----
"###
);
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,
@@ -887,7 +753,7 @@ impl TestCase {
Ok(())
}
fn root(&self) -> &Path {
fn project_dir(&self) -> &Path {
&self.project_dir
}

View File

@@ -9,14 +9,11 @@ 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};
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
use ruff_db::files::{system_path_to_file, File, FileError};
use ruff_db::source::source_text;
use ruff_db::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
};
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ruff_db::Upcast;
use ruff_python_ast::PythonVersion;
struct TestCase {
db: ProjectDatabase,
@@ -223,44 +220,17 @@ where
}
trait SetupFiles {
fn setup(self, context: &SetupContext) -> anyhow::Result<()>;
}
struct SetupContext<'a> {
system: &'a OsSystem,
root_path: &'a SystemPath,
}
impl<'a> SetupContext<'a> {
fn system(&self) -> &'a OsSystem {
self.system
}
fn join_project_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
self.project_path().join(relative)
}
fn project_path(&self) -> &SystemPath {
self.system.current_directory()
}
fn root_path(&self) -> &'a SystemPath {
self.root_path
}
fn join_root_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
self.root_path().join(relative)
}
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()>;
}
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
where
P: AsRef<SystemPath>,
{
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
fn setup(self, _root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
for (relative_path, content) in self {
let relative_path = relative_path.as_ref();
let absolute_path = context.join_project_path(relative_path);
let absolute_path = project_path.join(relative_path);
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for file `{relative_path}`")
@@ -280,10 +250,10 @@ where
impl<F> SetupFiles for F
where
F: FnOnce(&SetupContext) -> anyhow::Result<()>,
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
{
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
self(context)
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
self(root_path, project_path)
}
}
@@ -291,12 +261,13 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
where
F: SetupFiles,
{
setup_with_options(setup_files, |_context| None)
setup_with_options(setup_files, |_root, _project_path| None)
}
// TODO: Replace with configuration?
fn setup_with_options<F>(
setup_files: F,
create_options: impl FnOnce(&SetupContext) -> Option<Options>,
create_options: impl FnOnce(&SystemPath, &SystemPath) -> Option<Options>,
) -> anyhow::Result<TestCase>
where
F: SetupFiles,
@@ -324,17 +295,13 @@ where
std::fs::create_dir_all(project_path.as_std_path())
.with_context(|| format!("Failed to create project directory `{project_path}`"))?;
let system = OsSystem::new(&project_path);
let setup_context = SetupContext {
system: &system,
root_path: &root_path,
};
setup_files
.setup(&setup_context)
.setup(&root_path, &project_path)
.context("Failed to setup test files")?;
if let Some(options) = create_options(&setup_context) {
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 {
@@ -348,9 +315,7 @@ where
.context("Failed to write configuration")?;
}
let mut project = ProjectMetadata::discover(&project_path, &system)?;
project.apply_configuration_files(&system)?;
let project = ProjectMetadata::discover(&project_path, &system)?;
let program_settings = project.to_program_settings(&system);
for path in program_settings
@@ -824,12 +789,10 @@ fn directory_deleted() -> anyhow::Result<()> {
#[test]
fn search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
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(
context.join_root_path("site_packages"),
)]),
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -890,12 +853,10 @@ fn add_search_path() -> anyhow::Result<()> {
#[test]
fn remove_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
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(
context.join_root_path("site_packages"),
)]),
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -933,7 +894,7 @@ import os
print(sys.last_exc, os.getegid())
"#,
)],
|_context| {
|_root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
@@ -981,31 +942,21 @@ print(sys.last_exc, os.getegid())
#[test]
fn changed_versions_file() -> anyhow::Result<()> {
let mut case = setup_with_options(
|context: &SetupContext| {
|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())?;
std::fs::write(root_path.join("typeshed/stdlib/VERSIONS").as_std_path(), "")?;
std::fs::write(
context.join_project_path("bar.py").as_std_path(),
"import sub.a",
)?;
std::fs::create_dir_all(context.join_root_path("typeshed/stdlib").as_std_path())?;
std::fs::write(
context
.join_root_path("typeshed/stdlib/VERSIONS")
.as_std_path(),
"",
)?;
std::fs::write(
context
.join_root_path("typeshed/stdlib/os.pyi")
.as_std_path(),
root_path.join("typeshed/stdlib/os.pyi").as_std_path(),
"# not important",
)?;
Ok(())
},
|context| {
|root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
typeshed: Some(RelativePathBuf::cli(root_path.join("typeshed"))),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -1056,12 +1007,12 @@ fn changed_versions_file() -> anyhow::Result<()> {
/// we're seeing is that Windows only emits a single event, similar to Linux.
#[test]
fn hard_links_in_project() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let foo_path = context.join_project_path("foo.py");
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
let foo_path = project.join("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = context.join_project_path("bar.py");
let bar_path = project.join("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1127,12 +1078,12 @@ fn hard_links_in_project() -> anyhow::Result<()> {
ignore = "windows doesn't support observing changes to hard linked files."
)]
fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let foo_path = context.join_root_path("foo.py");
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
let foo_path = root.join("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = context.join_project_path("bar.py");
let bar_path = project.join("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1235,9 +1186,9 @@ mod unix {
ignore = "FSEvents doesn't emit change events for symlinked directories outside of the watched paths."
)]
fn symlink_target_outside_watched_paths() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let link_target = context.join_root_path("bar");
let link_target = root.join("bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1245,7 +1196,7 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside the project
let bar = context.join_project_path("bar");
let bar = project.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1316,9 +1267,9 @@ mod unix {
/// ```
#[test]
fn symlink_inside_project() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let link_target = context.join_project_path("patched/bar");
let link_target = project.join("patched/bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1326,7 +1277,7 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside site-packages
let bar_in_project = context.join_project_path("bar");
let bar_in_project = project.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_project.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1407,9 +1358,9 @@ mod unix {
#[test]
fn symlinked_module_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options(
|context: &SetupContext| {
|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let site_packages = context.join_root_path("site-packages");
let site_packages = root.join("site-packages");
let bar = site_packages.join("bar");
std::fs::create_dir_all(bar.as_std_path())
.context("Failed to create bar directory")?;
@@ -1418,8 +1369,7 @@ mod unix {
.context("Failed to write baz.py")?;
// Symlink the site packages in the venv to the global site packages
let venv_site_packages =
context.join_project_path(".venv/lib/python3.12/site-packages");
let venv_site_packages = project.join(".venv/lib/python3.12/site-packages");
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
.context("Failed to create .venv directory")?;
std::os::unix::fs::symlink(
@@ -1430,7 +1380,7 @@ mod unix {
Ok(())
},
|_context| {
|_root, _project| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(
@@ -1500,9 +1450,9 @@ mod unix {
#[test]
fn nested_projects_delete_root() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let mut case = setup(|root: &SystemPath, project_root: &SystemPath| {
std::fs::write(
context.join_project_path("pyproject.toml").as_std_path(),
project_root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "inner"
@@ -1512,7 +1462,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
)?;
std::fs::write(
context.join_root_path("pyproject.toml").as_std_path(),
root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "outer"
@@ -1537,79 +1487,3 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn changes_to_user_configuration() -> anyhow::Result<()> {
let mut _config_dir_override: Option<UserConfigDirectoryOverrideGuard> = None;
let mut case = setup(|context: &SetupContext| {
std::fs::write(
context.join_project_path("pyproject.toml").as_std_path(),
r#"
[project]
name = "test"
"#,
)?;
std::fs::write(
context.join_project_path("foo.py").as_std_path(),
"a = 10 / 0",
)?;
let config_directory = context.join_root_path("home/.config");
std::fs::create_dir_all(config_directory.join("knot").as_std_path())?;
std::fs::write(
config_directory.join("knot/knot.toml").as_std_path(),
r#"
[rules]
division-by-zero = "ignore"
"#,
)?;
_config_dir_override = Some(
context
.system()
.with_user_config_directory(Some(config_directory)),
);
Ok(())
})?;
let foo = case
.system_file(case.project_path("foo.py"))
.expect("foo.py to exist");
let diagnostics = case
.db()
.check_file(foo)
.context("Failed to check project.")?;
assert!(
diagnostics.is_empty(),
"Expected no diagnostics but got: {diagnostics:#?}"
);
// Enable division-by-zero in the user configuration with warning severity
update_file(
case.root_path().join("home/.config/knot/knot.toml"),
r#"
[rules]
division-by-zero = "warn"
"#,
)?;
let changes = case.stop_watch(event_for_file("knot.toml"));
case.apply_changes(changes);
let diagnostics = case
.db()
.check_file(foo)
.context("Failed to check project.")?;
assert!(
diagnostics.len() == 1,
"Expected exactly one diagnostic but got: {diagnostics:#?}"
);
Ok(())
}

View File

@@ -13,7 +13,7 @@ license.workspace = true
[dependencies]
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["cache", "serde"] }
ruff_db = { workspace = true, features = ["os", "cache", "serde"] }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
@@ -24,11 +24,10 @@ anyhow = { workspace = true }
crossbeam = { workspace = true }
glob = { workspace = true }
notify = { workspace = true }
pep440_rs = { workspace = true, features = ["version-ranges"] }
pep440_rs = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
@@ -41,9 +40,8 @@ insta = { workspace = true, features = ["redactions", "ron"] }
[features]
default = ["zstd"]
deflate = ["red_knot_vendored/deflate"]
schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
zstd = ["red_knot_vendored/zstd"]
deflate = ["red_knot_vendored/deflate"]
[lints]
workspace = true

View File

@@ -1,8 +1,7 @@
use std::{collections::HashMap, hash::BuildHasher};
use red_knot_python_semantic::{PythonPlatform, SitePackages};
use red_knot_python_semantic::{PythonPlatform, PythonVersion, SitePackages};
use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion;
/// Combine two values, preferring the values in `self`.
///

View File

@@ -114,8 +114,8 @@ impl SemanticDb for ProjectDatabase {
project.is_file_open(self, file)
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.project().rules(self)
fn rule_selection(&self) -> &RuleSelection {
self.project().rule_selection(self)
}
fn lint_registry(&self) -> &LintRegistry {
@@ -186,6 +186,7 @@ pub(crate) mod tests {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
rule_selection: RuleSelection,
project: Option<Project>,
}
@@ -197,6 +198,7 @@ pub(crate) mod tests {
vendored: red_knot_vendored::file_system().clone(),
files: Files::default(),
events: Arc::default(),
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
project: None,
};
@@ -268,8 +270,8 @@ pub(crate) mod tests {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.project().rules(self)
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {

View File

@@ -8,7 +8,6 @@ use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::system::walk_directory::WalkState;
use ruff_db::system::SystemPath;
use ruff_db::Db as _;
use ruff_python_ast::PySourceType;
use rustc_hash::FxHashSet;
impl ProjectDatabase {
@@ -48,7 +47,7 @@ impl ProjectDatabase {
if let Some(path) = change.system_path() {
if matches!(
path.file_name(),
Some(".gitignore" | ".ignore" | "knot.toml" | "pyproject.toml")
Some(".gitignore" | ".ignore" | "ruff.toml" | ".ruff.toml" | "pyproject.toml")
) {
// Changes to ignore files or settings can change the project structure or add/remove files.
project_changed = true;
@@ -145,12 +144,6 @@ impl ProjectDatabase {
metadata.apply_cli_options(cli_options.clone());
}
if let Err(error) = metadata.apply_configuration_files(self.system()) {
tracing::error!(
"Failed to apply configuration files, continuing without applying them: {error}"
);
}
let program_settings = metadata.to_program_settings(self.system());
let program = Program::get(self);
@@ -208,16 +201,9 @@ impl ProjectDatabase {
return WalkState::Continue;
}
if entry
.path()
.extension()
.and_then(PySourceType::try_from_extension)
.is_some()
{
let mut paths = added_paths.lock().unwrap();
let mut paths = added_paths.lock().unwrap();
paths.push(entry.into_path());
}
paths.push(entry.into_path());
WalkState::Continue
})

View File

@@ -3,18 +3,18 @@
use crate::metadata::options::OptionDiagnostic;
pub use db::{Db, ProjectDatabase};
use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings;
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity, Span};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
use ruff_db::source::{source_text, SourceTextError};
use ruff_db::system::walk_directory::WalkState;
use ruff_db::system::{FileType, SystemPath};
use ruff_python_ast::PySourceType;
use ruff_text_size::TextRange;
use rustc_hash::{FxBuildHasher, FxHashSet};
use salsa::Durability;
use salsa::Setter;
@@ -66,22 +66,12 @@ pub struct Project {
/// The metadata describing the project, including the unresolved options.
#[return_ref]
pub metadata: ProjectMetadata,
/// The resolved project settings.
#[return_ref]
pub settings: Settings,
/// Diagnostics that were generated when resolving the project settings.
#[return_ref]
settings_diagnostics: Vec<OptionDiagnostic>,
}
#[salsa::tracked]
impl Project {
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
Project::builder(metadata, settings, settings_diagnostics)
Project::builder(metadata)
.durability(Durability::MEDIUM)
.open_fileset_durability(Durability::LOW)
.file_set_durability(Durability::LOW)
@@ -96,37 +86,30 @@ impl Project {
self.metadata(db).name()
}
/// Returns the resolved linter rules for the project.
///
/// This is a salsa query to prevent re-computing queries if other, unrelated
/// settings change. For example, we don't want that changing the terminal settings
/// invalidates any type checking queries.
#[salsa::tracked]
pub fn rules(self, db: &dyn Db) -> Arc<RuleSelection> {
self.settings(db).to_rules()
}
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
tracing::debug!("Reloading project");
assert_eq!(self.root(db), metadata.root());
if &metadata != self.metadata(db) {
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
if self.settings(db) != &settings {
self.set_settings(db).to(settings);
}
if self.settings_diagnostics(db) != &settings_diagnostics {
self.set_settings_diagnostics(db).to(settings_diagnostics);
}
self.set_metadata(db).to(metadata);
}
self.reload_files(db);
}
pub fn rule_selection(self, db: &dyn Db) -> &RuleSelection {
let (selection, _) = self.rule_selection_with_diagnostics(db);
selection
}
#[salsa::tracked(return_ref)]
fn rule_selection_with_diagnostics(
self,
db: &dyn Db,
) -> (RuleSelection, Vec<OptionDiagnostic>) {
self.metadata(db).options().to_rule_selection(db)
}
/// Checks all open files in the project and its dependencies.
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
let project_span = tracing::debug_span!("Project::check");
@@ -135,7 +118,8 @@ impl Project {
tracing::debug!("Checking project '{name}'", name = self.name(db));
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
diagnostic
}));
@@ -167,8 +151,9 @@ impl Project {
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
let mut file_diagnostics: Vec<_> = self
.settings_diagnostics(db)
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
let mut file_diagnostics: Vec<_> = options_diagnostics
.iter()
.map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
@@ -344,13 +329,7 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
boxed
}));
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
diagnostics
}
@@ -463,8 +442,12 @@ impl Diagnostic for IOErrorDiagnostic {
self.error.to_string().into()
}
fn span(&self) -> Option<Span> {
Some(Span::from(self.file))
fn file(&self) -> Option<File> {
Some(self.file)
}
fn range(&self) -> Option<TextRange> {
None
}
fn severity(&self) -> Severity {

View File

@@ -1,4 +1,3 @@
use configuration_file::{ConfigurationFile, ConfigurationFileError};
use red_knot_python_semantic::ProgramSettings;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_python_ast::name::Name;
@@ -6,15 +5,13 @@ use std::sync::Arc;
use thiserror::Error;
use crate::combine::Combine;
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
use crate::metadata::pyproject::{Project, PyProject, PyProjectError};
use crate::metadata::value::ValueSource;
use options::KnotTomlError;
use options::Options;
mod configuration_file;
pub mod options;
pub mod pyproject;
pub mod settings;
pub mod value;
#[derive(Debug, PartialEq, Eq)]
@@ -26,15 +23,6 @@ pub struct ProjectMetadata {
/// The raw options
pub(super) options: Options,
/// Paths of configurations other than the project's configuration that were combined into [`Self::options`].
///
/// This field stores the paths of the configuration files, mainly for
/// knowing which files to watch for changes.
///
/// The path ordering doesn't imply precedence.
#[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))]
pub(super) extra_configuration_paths: Vec<SystemPathBuf>,
}
impl ProjectMetadata {
@@ -43,16 +31,12 @@ impl ProjectMetadata {
Self {
name,
root,
extra_configuration_paths: Vec::default(),
options: Options::default(),
}
}
/// Loads a project from a `pyproject.toml` file.
pub(crate) fn from_pyproject(
pyproject: PyProject,
root: SystemPathBuf,
) -> Result<Self, ResolveRequiresPythonError> {
pub(crate) fn from_pyproject(pyproject: PyProject, root: SystemPathBuf) -> Self {
Self::from_options(
pyproject
.tool
@@ -65,37 +49,21 @@ impl ProjectMetadata {
/// Loads a project from a set of options with an optional pyproject-project table.
pub(crate) fn from_options(
mut options: Options,
options: Options,
root: SystemPathBuf,
project: Option<&Project>,
) -> Result<Self, ResolveRequiresPythonError> {
) -> Self {
let name = project
.and_then(|project| project.name.as_deref())
.map(|name| Name::new(&**name))
.and_then(|project| project.name.as_ref())
.map(|name| Name::new(&***name))
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
// If the `options` don't specify a python version but the `project.requires-python` field is set,
// use that as a lower bound instead.
if let Some(project) = project {
if !options
.environment
.as_ref()
.is_some_and(|env| env.python_version.is_some())
{
if let Some(requires_python) = project.resolve_requires_python_lower_bound()? {
let mut environment = options.environment.unwrap_or_default();
environment.python_version = Some(requires_python);
options.environment = Some(environment);
}
}
}
Ok(Self {
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python
Self {
name,
root,
options,
extra_configuration_paths: Vec::new(),
})
}
}
/// Discovers the closest project at `path` and returns its metadata.
@@ -163,34 +131,19 @@ impl ProjectMetadata {
}
tracing::debug!("Found project at '{}'", project_root);
let metadata = ProjectMetadata::from_options(
return Ok(ProjectMetadata::from_options(
options,
project_root.to_path_buf(),
pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
)
.map_err(|err| {
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
source: err,
path: pyproject_path,
}
})?;
return Ok(metadata);
));
}
if let Some(pyproject) = pyproject {
let has_knot_section = pyproject.knot().is_some();
let metadata =
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
.map_err(
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
source: err,
path: pyproject_path,
},
)?;
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf());
if has_knot_section {
tracing::debug!("Found project at '{}'", project_root);
@@ -238,10 +191,6 @@ impl ProjectMetadata {
&self.options
}
pub fn extra_configuration_paths(&self) -> &[SystemPathBuf] {
&self.extra_configuration_paths
}
pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings {
self.options.to_program_settings(self.root(), system)
}
@@ -251,31 +200,9 @@ impl ProjectMetadata {
self.options = options.combine(std::mem::take(&mut self.options));
}
/// Applies the options from the configuration files to the project's options.
///
/// This includes:
///
/// * The user-level configuration
pub fn apply_configuration_files(
&mut self,
system: &dyn System,
) -> Result<(), ConfigurationFileError> {
if let Some(user) = ConfigurationFile::user(system)? {
tracing::debug!(
"Applying user-level configuration loaded from `{path}`.",
path = user.path()
);
self.apply_configuration_file(user);
}
Ok(())
}
/// Applies a lower-precedence configuration files to the project's options.
fn apply_configuration_file(&mut self, options: ConfigurationFile) {
self.extra_configuration_paths
.push(options.path().to_owned());
self.options.combine_with(options.into_options());
/// Combine the project options with the user options where project options take precedence.
pub fn apply_user_options(&mut self, options: Options) {
self.options.combine_with(options);
}
}
@@ -295,22 +222,16 @@ pub enum ProjectDiscoveryError {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
#[error("Invalid `requires-python` version specifier (`{path}`): {source}")]
InvalidRequiresPythonConstraint {
source: ResolveRequiresPythonError,
path: SystemPathBuf,
},
}
#[cfg(test)]
mod tests {
//! Integration tests for project discovery
use crate::snapshot_project;
use anyhow::{anyhow, Context};
use insta::assert_ron_snapshot;
use ruff_db::system::{SystemPathBuf, TestSystem};
use ruff_python_ast::PythonVersion;
use crate::{ProjectDiscoveryError, ProjectMetadata};
@@ -329,15 +250,7 @@ mod tests {
assert_eq!(project.root(), &*root);
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)
"#);
});
snapshot_project!(project);
Ok(())
}
@@ -366,16 +279,7 @@ mod tests {
ProjectMetadata::discover(&root, &system).context("Failed to discover project")?;
assert_eq!(project.root(), &*root);
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)
"#);
});
snapshot_project!(project);
// Discovering the same package from a subdirectory should give the same result
let from_src = ProjectMetadata::discover(&root.join("db"), &system)
@@ -458,19 +362,7 @@ expected `.`, `]`
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(sub_project, @r#"
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
snapshot_project!(sub_project);
Ok(())
}
@@ -508,19 +400,7 @@ expected `.`, `]`
let root = ProjectMetadata::discover(&root, &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
snapshot_project!(root);
Ok(())
}
@@ -552,15 +432,7 @@ expected `.`, `]`
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(sub_project, @r#"
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(),
)
"#);
});
snapshot_project!(sub_project);
Ok(())
}
@@ -595,19 +467,7 @@ expected `.`, `]`
let root = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.10"),
)),
),
)
"#);
});
snapshot_project!(root);
Ok(())
}
@@ -627,304 +487,27 @@ expected `.`, `]`
(
root.join("pyproject.toml"),
r#"
[project]
name = "super-app"
requires-python = ">=3.12"
[project]
name = "super-app"
requires-python = ">=3.12"
[tool.knot.src]
root = "this_option_is_ignored"
"#,
[tool.knot.src]
root = "this_option_is_ignored"
"#,
),
(
root.join("knot.toml"),
r#"
[src]
root = "src"
"#,
[src]
root = "src"
"#,
),
])
.context("Failed to write files")?;
let root = ProjectMetadata::discover(&root, &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("super-app"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.12"),
)),
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
Ok(())
}
#[test]
fn requires_python_major_minor() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
#[test]
fn requires_python_major_only() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::from((3, 0)))
);
Ok(())
}
/// A `requires-python` constraint with major, minor and patch can be simplified
/// to major and minor (e.g. 3.12.1 -> 3.12).
#[test]
fn requires_python_major_minor_patch() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12.8"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
#[test]
fn requires_python_beta_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">= 3.13.0b0"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY313)
);
Ok(())
}
#[test]
fn requires_python_greater_than_major_minor() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
# This is somewhat nonsensical because 3.12.1 > 3.12 is true.
# That's why simplifying the constraint to >= 3.12 is correct
requires-python = ">3.12"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
/// `python-version` takes precedence if both `requires-python` and `python-version` are configured.
#[test]
fn requires_python_and_python_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12"
[tool.knot.environment]
python-version = "3.10"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY310)
);
Ok(())
}
#[test]
fn requires_python_less_than() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = "<3.12"
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because the `requires-python` doesn't specify a lower bound (it only specifies an upper bound)."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `<3.12` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.");
Ok(())
}
#[test]
fn requires_python_no_specifiers() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ""
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because the `requires-python` specifiers are empty and don't define a lower bound."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.");
Ok(())
}
#[test]
fn requires_python_too_large_major_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=999.0"
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because of the requires-python major version that is larger than 255."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): The major version `999` is larger than the maximum supported value 255");
snapshot_project!(root);
Ok(())
}
@@ -934,12 +517,15 @@ expected `.`, `]`
assert_eq!(error.to_string().replace('\\', "/"), message);
}
fn with_escaped_paths<R>(f: impl FnOnce() -> R) -> R {
let mut settings = insta::Settings::clone_current();
settings.add_dynamic_redaction(".root", |content, _path| {
content.as_str().unwrap().replace('\\', "/")
/// Snapshots a project but with all paths using unix separators.
#[macro_export]
macro_rules! snapshot_project {
($project:expr) => {{
assert_ron_snapshot!($project,{
".root" => insta::dynamic_redaction(|content, _content_path| {
content.as_str().unwrap().replace("\\", "/")
}),
});
settings.bind(f)
}
}};
}
}

View File

@@ -1,69 +0,0 @@
use std::sync::Arc;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use thiserror::Error;
use crate::metadata::value::ValueSource;
use super::options::{KnotTomlError, Options};
/// A `knot.toml` configuration file with the options it contains.
pub(crate) struct ConfigurationFile {
path: SystemPathBuf,
options: Options,
}
impl ConfigurationFile {
/// Loads the user-level configuration file if it exists.
///
/// Returns `None` if the file does not exist or if the concept of user-level configurations
/// doesn't exist on `system`.
pub(crate) fn user(system: &dyn System) -> Result<Option<Self>, ConfigurationFileError> {
let Some(configuration_directory) = system.user_config_directory() else {
return Ok(None);
};
let knot_toml_path = configuration_directory.join("knot").join("knot.toml");
tracing::debug!(
"Searching for a user-level configuration at `{path}`",
path = &knot_toml_path
);
let Ok(knot_toml_str) = system.read_to_string(&knot_toml_path) else {
return Ok(None);
};
match Options::from_toml_str(
&knot_toml_str,
ValueSource::File(Arc::new(knot_toml_path.clone())),
) {
Ok(options) => Ok(Some(Self {
path: knot_toml_path,
options,
})),
Err(error) => Err(ConfigurationFileError::InvalidKnotToml {
source: Box::new(error),
path: knot_toml_path,
}),
}
}
/// Returns the path to the configuration file.
pub(crate) fn path(&self) -> &SystemPath {
&self.path
}
pub(crate) fn into_options(self) -> Options {
self.options
}
}
#[derive(Debug, Error)]
pub enum ConfigurationFileError {
#[error("{path} is not a valid `knot.toml`: {source}")]
InvalidKnotToml {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
}

View File

@@ -1,38 +1,32 @@
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, SearchPathSettings, SitePackages};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::system_path_to_file;
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_python_ast::PythonVersion;
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::Debug;
use thiserror::Error;
use super::settings::{Settings, TerminalSettings};
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
/// Configures the type checking environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<EnvironmentOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<SrcOptions>,
/// Configures the enabled lints and their severity.
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,
}
impl Options {
@@ -113,22 +107,7 @@ impl Options {
}
#[must_use]
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
let (rules, diagnostics) = self.to_rule_selection(db);
let mut settings = Settings::new(rules);
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
});
}
(settings, diagnostics)
}
#[must_use]
fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
let registry = db.lint_registry();
let mut diagnostics = Vec::new();
@@ -187,14 +166,7 @@ impl Options {
),
};
let span = file.map(Span::from).map(|span| {
if let Some(range) = rule_name.range() {
span.with_range(range)
} else {
span
}
});
diagnostics.push(diagnostic.with_span(span));
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
}
}
}
@@ -205,22 +177,10 @@ impl Options {
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EnvironmentOptions {
/// Specifies the version of Python that will be used to execute the source code.
/// The version should be specified as a string in the format `M.m` where `M` is the major version
/// and `m` is the minor (e.g. "3.0" or "3.6").
/// If a version is provided, knot will generate errors if the source code makes use of language features
/// that are not supported in that version.
/// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.
#[serde(skip_serializing_if = "Option::is_none")]
pub python_version: Option<RangedValue<PythonVersion>>,
/// Specifies the target platform that will be used to execute the source code.
/// If specified, Red Knot will tailor its use of type stub files,
/// which conditionalize type definitions based on the platform.
///
/// If no platform is specified, knot will use `all` or the current platform in the LSP use case.
#[serde(skip_serializing_if = "Option::is_none")]
pub python_platform: Option<RangedValue<PythonPlatform>>,
@@ -244,7 +204,6 @@ pub struct EnvironmentOptions {
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SrcOptions {
/// The root of the project, used for finding first-party modules.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -253,9 +212,7 @@ pub struct SrcOptions {
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Rules {
#[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
}
@@ -269,79 +226,6 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TerminalOptions {
/// Use exit code 1 if there are any warning-level diagnostics.
///
/// Defaults to `false`.
pub error_on_warning: Option<bool>,
}
#[cfg(feature = "schemars")]
mod schema {
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::Level;
use schemars::gen::SchemaGenerator;
use schemars::schema::{
InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation,
};
use schemars::JsonSchema;
pub(super) struct Rules;
impl JsonSchema for Rules {
fn schema_name() -> String {
"Rules".to_string()
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let registry = &*DEFAULT_LINT_REGISTRY;
let level_schema = gen.subschema_for::<Level>();
let properties: schemars::Map<String, Schema> = registry
.lints()
.iter()
.map(|lint| {
(
lint.name().to_string(),
Schema::Object(SchemaObject {
metadata: Some(Box::new(Metadata {
title: Some(lint.summary().to_string()),
description: Some(lint.documentation()),
deprecated: lint.status.is_deprecated(),
default: Some(lint.default_level.to_string().into()),
..Metadata::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![level_schema.clone()]),
..Default::default()
})),
..Default::default()
}),
)
})
.collect();
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties,
// Allow unknown rules: Red Knot will warn about them.
// It gives a better experience when using an older Red Knot version because
// the schema will not deny rules that have been removed in newer versions.
additional_properties: Some(Box::new(level_schema)),
..ObjectValidation::default()
})),
..Default::default()
})
}
}
}
#[derive(Error, Debug)]
pub enum KnotTomlError {
#[error(transparent)]
@@ -353,7 +237,8 @@ pub struct OptionDiagnostic {
id: DiagnosticId,
message: String,
severity: Severity,
span: Option<Span>,
file: Option<File>,
range: Option<TextRange>,
}
impl OptionDiagnostic {
@@ -362,13 +247,21 @@ impl OptionDiagnostic {
id,
message,
severity,
span: None,
file: None,
range: None,
}
}
#[must_use]
fn with_span(self, span: Option<Span>) -> Self {
OptionDiagnostic { span, ..self }
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
}
}
@@ -381,8 +274,12 @@ impl Diagnostic for OptionDiagnostic {
Cow::Borrowed(&self.message)
}
fn span(&self) -> Option<Span> {
self.span.clone()
fn file(&self) -> Option<File> {
self.file
}
fn range(&self) -> Option<TextRange> {
self.range
}
fn severity(&self) -> Severity {

View File

@@ -1,12 +1,11 @@
use crate::metadata::options::Options;
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers};
use ruff_python_ast::PythonVersion;
use pep440_rs::{Version, VersionSpecifiers};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::Bound;
use std::ops::Deref;
use thiserror::Error;
use crate::metadata::options::Options;
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
/// A `pyproject.toml` as specified in PEP 517.
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[serde(rename_all = "kebab-case")]
@@ -56,73 +55,6 @@ pub struct Project {
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
}
impl Project {
pub(super) fn resolve_requires_python_lower_bound(
&self,
) -> Result<Option<RangedValue<PythonVersion>>, ResolveRequiresPythonError> {
let Some(requires_python) = self.requires_python.as_ref() else {
return Ok(None);
};
tracing::debug!("Resolving requires-python constraint: `{requires_python}`");
let ranges = release_specifiers_to_ranges((**requires_python).clone());
let Some((lower, _)) = ranges.bounding_range() else {
return Ok(None);
};
let version = match lower {
// Ex) `>=3.10.1` -> `>=3.10`
Bound::Included(version) => version,
// Ex) `>3.10.1` -> `>=3.10` or `>3.10` -> `>=3.10`
// The second example looks obscure at first but it is required because
// `3.10.1 > 3.10` is true but we only have two digits here. So including 3.10 is the
// right move. Overall, using `>` without a patch release is most likely bogus.
Bound::Excluded(version) => version,
// Ex) `<3.10` or ``
Bound::Unbounded => {
return Err(ResolveRequiresPythonError::NoLowerBound(
requires_python.to_string(),
))
}
};
// Take the major and minor version
let mut versions = version.release().iter().take(2);
let Some(major) = versions.next().copied() else {
return Ok(None);
};
let minor = versions.next().copied().unwrap_or_default();
tracing::debug!("Resolved requires-python constraint to: {major}.{minor}");
let major =
u8::try_from(major).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(major))?;
let minor =
u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(minor))?;
Ok(Some(
requires_python
.clone()
.map_value(|_| PythonVersion::from((major, minor))),
))
}
}
#[derive(Debug, Error)]
pub enum ResolveRequiresPythonError {
#[error("The major version `{0}` is larger than the maximum supported value 255")]
TooLargeMajor(u64),
#[error("The minor version `{0}` is larger than the maximum supported value 255")]
TooLargeMinor(u64),
#[error("value `{0}` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.")]
NoLowerBound(String),
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {

View File

@@ -1,53 +0,0 @@
use std::sync::Arc;
use red_knot_python_semantic::lint::RuleSelection;
/// The resolved [`super::Options`] for the project.
///
/// Unlike [`super::Options`], the struct has default values filled in and
/// uses representations that are optimized for reads (instead of preserving the source representation).
/// It's also not required that this structure precisely resembles the TOML schema, although
/// it's encouraged to use a similar structure.
///
/// It's worth considering to adding a salsa query for specific settings to
/// limit the blast radius when only some settings change. For example,
/// changing the terminal settings shouldn't invalidate any core type-checking queries.
/// This can be achieved by adding a salsa query for the type checking specific settings.
///
/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settings {
rules: Arc<RuleSelection>,
terminal: TerminalSettings,
}
impl Settings {
pub fn new(rules: RuleSelection) -> Self {
Self {
rules: Arc::new(rules),
terminal: TerminalSettings::default(),
}
}
pub fn rules(&self) -> &RuleSelection {
&self.rules
}
pub fn to_rules(&self) -> Arc<RuleSelection> {
self.rules.clone()
}
pub fn terminal(&self) -> &TerminalSettings {
&self.terminal
}
pub fn set_terminal(&mut self, terminal: TerminalSettings) {
self.terminal = terminal;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TerminalSettings {
pub error_on_warning: bool,
}

View File

@@ -1,9 +1,8 @@
use crate::combine::Combine;
use crate::Db;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_macros::Combine;
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Deserializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::fmt;
@@ -71,19 +70,15 @@ impl Drop for ValueSourceGuard {
///
/// 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, serde::Serialize)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone)]
pub struct RangedValue<T> {
value: T,
#[serde(skip)]
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.
#[serde(skip)]
range: Option<TextRange>,
}
@@ -118,15 +113,6 @@ impl<T> RangedValue<T> {
self
}
#[must_use]
pub fn map_value<R>(self, f: impl FnOnce(T) -> R) -> RangedValue<R> {
RangedValue {
value: f(self.value),
source: self.source,
range: self.range,
}
}
pub fn into_inner(self) -> T {
self.value
}
@@ -280,6 +266,18 @@ where
}
}
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
@@ -288,19 +286,9 @@ where
/// * 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,
Combine,
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);
impl RelativePathBuf {
@@ -337,3 +325,13 @@ impl RelativePathBuf {
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

@@ -0,0 +1,13 @@
---
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

@@ -0,0 +1,13 @@
---
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

@@ -0,0 +1,13 @@
---
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

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

View File

@@ -0,0 +1,13 @@
---
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

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

View File

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

View File

@@ -73,13 +73,6 @@ impl ProjectWatcher {
.canonicalize_path(&project_path)
.unwrap_or(project_path);
let config_paths = db
.project()
.metadata(db)
.extra_configuration_paths()
.iter()
.cloned();
// Find the non-overlapping module search paths and filter out paths that are already covered by the project.
// Module search paths are already canonicalized.
let unique_module_paths = ruff_db::system::deduplicate_nested_paths(
@@ -90,11 +83,8 @@ impl ProjectWatcher {
.map(SystemPath::to_path_buf);
// Now add the new paths, first starting with the project path and then
// adding the library search paths, and finally the paths for configurations.
for path in std::iter::once(project_path)
.chain(unique_module_paths)
.chain(config_paths)
{
// adding the library search paths.
for path in std::iter::once(project_path).chain(unique_module_paths) {
// Log a warning. It's not worth aborting if registering a single folder fails because
// Ruff otherwise stills works as expected.
if let Err(error) = self.watcher.watch(&path) {

View File

@@ -270,8 +270,6 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
/// Whether or not the .py/.pyi version of this file is expected to fail
#[rustfmt::skip]
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
// related to circular references in nested functions
("crates/ruff_linter/resources/test/fixtures/flake8_return/RET503.py", false, true),
// related to circular references in class definitions
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),

View File

@@ -12,9 +12,9 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_index = { workspace = true, features = ["salsa"] }
ruff_index = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["salsa"] }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_source_file = { workspace = true }
@@ -31,12 +31,11 @@ drop_bomb = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true }
salsa = { workspace = true, features = ["compact_str"] }
salsa = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
hashbrown = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
@@ -44,7 +43,7 @@ test-case = { workspace = true }
memchr = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
ruff_db = { workspace = true, features = ["os", "testing"] }
ruff_python_parser = { workspace = true }
red_knot_test = { workspace = true }
red_knot_vendored = { workspace = true }
@@ -57,7 +56,7 @@ quickcheck = { version = "1.0.3", default-features = false }
quickcheck_macros = { version = "1.0.0" }
[features]
serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"]
serde = ["ruff_db/serde", "dep:serde"]
[lints]
workspace = true

View File

@@ -1,90 +0,0 @@
# Special cases for int/float/complex in annotations
In order to support common use cases, an annotation of `float` actually means `int | float`, and an
annotation of `complex` actually means `int | float | complex`. See
[the specification](https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
## float
An annotation of `float` means `int | float`, so `int` is assignable to it:
```py
def takes_float(x: float):
pass
def passes_int_to_float(x: int):
# no error!
takes_float(x)
```
It also applies to variable annotations:
```py
def assigns_int_to_float(x: int):
# no error!
y: float = x
```
It doesn't work the other way around:
```py
def takes_int(x: int):
pass
def passes_float_to_int(x: float):
# error: [invalid-argument-type]
takes_int(x)
def assigns_float_to_int(x: float):
# error: [invalid-assignment]
y: int = x
```
Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float`
as just `float`; we display the actual type:
```py
def f(x: float):
reveal_type(x) # revealed: int | float
```
## complex
An annotation of `complex` means `int | float | complex`, so `int` and `float` are both assignable
to it (but not the other way around):
```py
def takes_complex(x: complex):
pass
def passes_to_complex(x: float, y: int):
# no errors!
takes_complex(x)
takes_complex(y)
def assigns_to_complex(x: float, y: int):
# no errors!
a: complex = x
b: complex = y
def takes_int(x: int):
pass
def takes_float(x: float):
pass
def passes_complex(x: complex):
# error: [invalid-argument-type]
takes_int(x)
# error: [invalid-argument-type]
takes_float(x)
def assigns_complex(x: complex):
# error: [invalid-assignment]
y: int = x
# error: [invalid-assignment]
z: float = x
def f(x: complex):
reveal_type(x) # revealed: int | float | complex
```

View File

@@ -73,12 +73,12 @@ qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(decorated method)
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method)
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
```
### Assignability

View File

@@ -9,9 +9,9 @@ from typing import Union
a: Union[int, str]
a1: Union[int, bool]
a2: Union[int, Union[bytes, str]]
a2: Union[int, Union[float, str]]
a3: Union[int, None]
a4: Union[Union[bytes, str]]
a4: Union[Union[float, str]]
a5: Union[int]
a6: Union[()]
@@ -21,11 +21,11 @@ def f():
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
# revealed: int
reveal_type(a1)
# revealed: int | bytes | str
# revealed: int | float | str
reveal_type(a2)
# revealed: int | None
reveal_type(a3)
# revealed: bytes | str
# revealed: float | str
reveal_type(a4)
# revealed: int
reveal_type(a5)

View File

@@ -9,7 +9,7 @@ reveal_type(x) # revealed: Literal[2]
x = 1.0
x /= 2
reveal_type(x) # revealed: int | float
reveal_type(x) # revealed: float
```
## Dunder methods
@@ -24,12 +24,12 @@ x -= 1
reveal_type(x) # revealed: str
class C:
def __iadd__(self, other: str) -> int:
return 1
def __iadd__(self, other: str) -> float:
return 1.0
x = C()
x += "Hello"
reveal_type(x) # revealed: int
reveal_type(x) # revealed: float
```
## Unsupported types
@@ -40,7 +40,7 @@ class C:
return 42
x = C()
# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`"
# error: [invalid-argument-type]
x -= 1
reveal_type(x) # revealed: int
@@ -130,10 +130,10 @@ def _(flag: bool):
if flag:
f = Foo()
else:
f = 42
f = 42.0
f += 12
reveal_type(f) # revealed: str | Literal[54]
reveal_type(f) # revealed: str | float
```
## Partially bound target union with `__add__`

View File

@@ -30,11 +30,7 @@ reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# There is no special handling of attributes that are (directly) assigned to a declared parameter,
# which means we union with `Unknown` here, since the attribute itself is not declared. This is
# something that we might want to change in the future.
#
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
# TODO: should be `int | None`
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.declared_only) # revealed: bytes
@@ -49,10 +45,10 @@ reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
c_instance.inferred_from_value = "value set on instance"
# This assignment is also fine:
c_instance.declared_and_bound = False
c_instance.inferred_from_param = None
# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `declared_and_bound` of type `bool`"
c_instance.declared_and_bound = "incompatible"
# TODO: this should be an error (incompatible types in assignment)
c_instance.inferred_from_param = "incompatible"
# TODO: we already show an error here but the message might be improved?
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
@@ -185,6 +181,7 @@ reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a
# TODO: Should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# TODO: Should be `int | None`
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.declared_only) # revealed: bytes
@@ -235,36 +232,6 @@ reveal_type(c_instance.y) # revealed: int
reveal_type(c_instance.z) # revealed: int
```
#### Attributes defined in multi-target assignments
```py
class C:
def __init__(self) -> None:
self.a = self.b = 1
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b) # revealed: Unknown | Literal[1]
```
#### Augmented assignments
```py
class Weird:
def __iadd__(self, other: None) -> str:
return "a"
class C:
def __init__(self) -> None:
self.w = Weird()
self.w += None
# TODO: Mypy and pyright do not support this, but it would be great if we could
# infer `Unknown | str` or at least `Unknown | Weird | str` here.
reveal_type(C().w) # revealed: Unknown | Weird
```
#### Attributes defined in tuple unpackings
```py
@@ -286,24 +253,19 @@ reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"]
reveal_type(c_instance.c1) # revealed: Unknown | int
reveal_type(c_instance.d1) # revealed: Unknown | str
reveal_type(c_instance.a2) # revealed: Unknown | Literal[1]
# TODO: This should be supported (no error; type should be: `Unknown | Literal[1]`)
# error: [unresolved-attribute]
reveal_type(c_instance.a2) # revealed: Unknown
reveal_type(c_instance.b2) # revealed: Unknown | Literal["a"]
# TODO: This should be supported (no error; type should be: `Unknown | Literal["a"]`)
# error: [unresolved-attribute]
reveal_type(c_instance.b2) # revealed: Unknown
reveal_type(c_instance.c2) # revealed: Unknown | int
reveal_type(c_instance.d2) # revealed: Unknown | str
```
#### Starred assignments
```py
class C:
def __init__(self) -> None:
self.a, *self.b = (1, 2, 3)
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
# TODO: Similar for these two (should be `Unknown | int` and `Unknown | str`, respectively)
# error: [unresolved-attribute]
reveal_type(c_instance.c2) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(c_instance.d2) # revealed: Unknown
```
#### Attributes defined in for-loop (unpacking)
@@ -325,8 +287,6 @@ class TupleIterable:
def __iter__(self) -> TupleIterator:
return TupleIterator()
class NonIterable: ...
class C:
def __init__(self):
for self.x in IntIterable():
@@ -335,54 +295,14 @@ class C:
for _, self.y in TupleIterable():
pass
# TODO: We should emit a diagnostic here
for self.z in NonIterable():
pass
# TODO: Pyright fully supports these, mypy detects the presence of the attributes,
# but infers type `Any` for both of them. We should infer `int` and `str` here:
reveal_type(C().x) # revealed: Unknown | int
reveal_type(C().y) # revealed: Unknown | str
```
#### Attributes defined in `with` statements
```py
class ContextManager:
def __enter__(self) -> int | None: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
class C:
def __init__(self) -> None:
with ContextManager() as self.x:
pass
c_instance = C()
# TODO: Should be `Unknown | int | None`
# error: [unresolved-attribute]
reveal_type(c_instance.x) # revealed: Unknown
```
reveal_type(C().x) # revealed: Unknown
#### Attributes defined in comprehensions
```py
class IntIterator:
def __next__(self) -> int:
return 1
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class C:
def __init__(self) -> None:
[... for self.a in IntIterable()]
c_instance = C()
# TODO: Should be `Unknown | int`
# error: [unresolved-attribute]
reveal_type(c_instance.a) # revealed: Unknown
reveal_type(C().y) # revealed: Unknown
```
#### Conditionally declared / bound attributes
@@ -473,6 +393,8 @@ reveal_type(D().x) # revealed: Unknown | Literal[1]
If `staticmethod` is something else, that should not influence the behavior:
`other.py`:
```py
def staticmethod(f):
return f
@@ -487,6 +409,8 @@ reveal_type(C().x) # revealed: Unknown | Literal[1]
And if `staticmethod` is fully qualified, that should also be recognized:
`fully_qualified.py`:
```py
import builtins
@@ -523,15 +447,6 @@ class C:
reveal_type(C().x) # revealed: str
```
#### Diagnostics are reported for the right-hand side of attribute assignments
```py
class C:
def __init__(self) -> None:
# error: [too-many-positional-arguments]
self.x: int = len(1, 2, 3)
```
### Pure class variables (`ClassVar`)
#### Annotated with `ClassVar` type qualifier
@@ -807,67 +722,6 @@ def _(flag: bool, flag1: bool, flag2: bool):
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
```
### Attribute possibly unbound on a subclass but not on a superclass
```py
def _(flag: bool):
class Foo:
x = 1
class Bar(Foo):
if flag:
x = 2
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
```
### Attribute possibly unbound on a subclass and on a superclass
```py
def _(flag: bool):
class Foo:
if flag:
x = 1
class Bar(Foo):
if flag:
x = 2
# error: [possibly-unbound-attribute]
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
```
### Attribute access on `Any`
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on
`object` -- but if the attribute *does* exist on `object`, the type of the attribute is
`<type as it exists on object> & Any`.
```py
from typing import Any
class Foo(Any): ...
reveal_type(Foo.bar) # revealed: Any
reveal_type(Foo.__repr__) # revealed: Literal[__repr__] & Any
```
Similar principles apply if `Any` appears in the middle of an inheritance hierarchy:
```py
from typing import ClassVar, Literal
class A:
x: ClassVar[Literal[1]] = 1
class B(Any): ...
class C(B, A): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A], Literal[object]]
reveal_type(C.x) # revealed: Literal[1] & Any
```
### Unions with all paths unbound
If the symbol is unbound in all elements of the union, we detect that:
@@ -995,6 +849,8 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
functions are instances of that class:
`a.py`:
```py
def f(): ...
@@ -1004,9 +860,13 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(f.__call__) # revealed: <bound method `__call__` of `Literal[f]`>
def f(): ...
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
```
### Int-literal attributes
@@ -1014,13 +874,17 @@ reveal_type(f.__call__) # revealed: <bound method `__call__` of `Literal[f]`>
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
integers are instances of that class:
`a.py`:
```py
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).denominator) # revealed: @Todo(@property)
```
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type((2).numerator) # revealed: Literal[2]
reveal_type((2).real) # revealed: Literal[2]
@@ -1029,15 +893,19 @@ reveal_type((2).real) # revealed: Literal[2]
### Bool-literal attributes
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bools are instances of that class:
bols are instances of that class:
`a.py`:
```py
reveal_type(True.__and__) # revealed: @Todo(decorated method)
reveal_type(False.__or__) # revealed: @Todo(decorated method)
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
```
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type(True.numerator) # revealed: Literal[1]
reveal_type(False.real) # revealed: Literal[0]
@@ -1045,11 +913,11 @@ reveal_type(False.real) # revealed: Literal[0]
### Bytes-literal attributes
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
```py
reveal_type(b"foo".join) # revealed: <bound method `join` of `Literal[b"foo"]`>
reveal_type(b"foo".endswith) # revealed: <bound method `endswith` of `Literal[b"foo"]`>
reveal_type(b"foo".join) # revealed: @Todo(bound method)
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
```
## Instance attribute edge cases
@@ -1136,40 +1004,6 @@ class C:
reveal_type(C().x) # revealed: Unknown
```
### Builtin types attributes
This test can probably be removed eventually, but we currently include it because we do not yet
understand generic bases and protocols, and we want to make sure that we can still use builtin types
in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more
information.
```py
class C:
a_int: int = 1
a_str: str = "a"
a_bytes: bytes = b"a"
a_bool: bool = True
a_float: float = 1.0
a_complex: complex = 1 + 1j
a_tuple: tuple[int] = (1,)
a_range: range = range(1)
a_slice: slice = slice(1)
a_type: type = int
a_none: None = None
reveal_type(C.a_int) # revealed: int
reveal_type(C.a_str) # revealed: str
reveal_type(C.a_bytes) # revealed: bytes
reveal_type(C.a_bool) # revealed: bool
reveal_type(C.a_float) # revealed: int | float
reveal_type(C.a_complex) # revealed: int | float | complex
reveal_type(C.a_tuple) # revealed: tuple[int]
reveal_type(C.a_range) # revealed: range
reveal_type(C.a_slice) # revealed: slice
reveal_type(C.a_type) # revealed: type
reveal_type(C.a_none) # revealed: None
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -56,7 +56,7 @@ def _(a: 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 | float
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
@@ -64,7 +64,7 @@ def _(a: 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 | float
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
@@ -72,7 +72,7 @@ def _(a: 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 | float
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_bool(x: bool):
@@ -80,7 +80,7 @@ def _(a: 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 | float
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
@@ -88,6 +88,6 @@ def _(a: 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 | float
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
```

View File

@@ -244,7 +244,10 @@ class B:
def __rsub__(self, other: A) -> B:
return B()
reveal_type(A() - B()) # revealed: B
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
# because `A.__sub__` is annotated as only accepting `A`,
# but `B.__rsub__` will accept `A`.
reveal_type(A() - B()) # revealed: A
```
## Callable instances as dunders
@@ -260,31 +263,31 @@ class B:
__add__ = A()
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg.
# Revealed type should be `Unknown | int`.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`"
reveal_type(B() + B()) # revealed: Unknown
reveal_type(B() + B()) # revealed: Unknown | int
```
## Integration test: numbers from typeshed
We get less precise results from binary operations on float/complex literals due to the special case
for annotations of `float` or `complex`, which applies also to return annotations for typeshed
dunder methods. Perhaps we could have a special-case on the special-case, to exclude these typeshed
return annotations from the widening, and preserve a bit more precision here?
```py
reveal_type(3j + 3.14) # revealed: int | float | complex
reveal_type(4.2 + 42) # revealed: int | float
reveal_type(3j + 3) # revealed: int | float | complex
reveal_type(3.14 + 3j) # revealed: int | float | complex
reveal_type(42 + 4.2) # revealed: int | float
reveal_type(3 + 3j) # revealed: int | float | complex
reveal_type(3j + 3.14) # revealed: complex
reveal_type(4.2 + 42) # revealed: float
reveal_type(3j + 3) # revealed: complex
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3.14 + 3j) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(42 + 4.2) # revealed: int
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
def _(x: bool, y: int):
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: int | float
reveal_type(y + 4.12) # revealed: int | float
reveal_type(4.2 + x) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
```
## With literal types
@@ -301,7 +304,8 @@ class A:
return self
reveal_type(A() + 1) # revealed: A
reveal_type(1 + A()) # revealed: A
# TODO should be `A` since `int.__add__` doesn't support `A` instances
reveal_type(1 + A()) # revealed: int
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances

View File

@@ -10,16 +10,16 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: Should emit `unsupported-operator` but we don't understand the bases of `str`, so we think
# it inherits `Unknown`, so we think `str.__radd__` is `Unknown` instead of nonexistent.
reveal_type(2 + "f") # revealed: Unknown
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: int
def lhs(x: int):
reveal_type(x + 1) # revealed: int
reveal_type(x - 4) # revealed: int
reveal_type(x * -1) # revealed: int
reveal_type(x // 3) # revealed: int
reveal_type(x / 3) # revealed: int | float
reveal_type(x / 3) # revealed: float
reveal_type(x % 3) # revealed: int
def rhs(x: int):
@@ -27,7 +27,7 @@ def rhs(x: int):
reveal_type(3 - x) # revealed: int
reveal_type(3 * x) # revealed: int
reveal_type(-3 // x) # revealed: int
reveal_type(-3 / x) # revealed: int | float
reveal_type(-3 / x) # revealed: float
reveal_type(5 % x) # revealed: int
def both(x: int):
@@ -35,7 +35,7 @@ def both(x: int):
reveal_type(x - x) # revealed: int
reveal_type(x * x) # revealed: int
reveal_type(x // x) # revealed: int
reveal_type(x / x) # revealed: int | float
reveal_type(x / x) # revealed: float
reveal_type(x % x) # revealed: int
```
@@ -80,20 +80,24 @@ c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
reveal_type(c) # revealed: int
# error: "Cannot divide object of type `int` by zero"
reveal_type(int() / 0) # revealed: int | float
# revealed: float
reveal_type(int() / 0)
# error: "Cannot divide object of type `Literal[1]` by zero"
reveal_type(1 / False) # revealed: float
# revealed: float
reveal_type(1 / False)
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
True / False
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
bool(1) / False
# error: "Cannot divide object of type `float` by zero"
reveal_type(1.0 / 0) # revealed: int | float
# revealed: float
reveal_type(1.0 / 0)
class MyInt(int): ...
# No error for a subclass of int
reveal_type(MyInt(3) / 0) # revealed: int | float
# revealed: float
reveal_type(MyInt(3) / 0)
```

View File

@@ -4,14 +4,14 @@
```py
class Multiplier:
def __init__(self, factor: int):
def __init__(self, factor: float):
self.factor = factor
def __call__(self, number: int) -> int:
def __call__(self, number: float) -> float:
return number * self.factor
a = Multiplier(2)(3)
reveal_type(a) # revealed: int
a = Multiplier(2.0)(3.0)
reveal_type(a) # revealed: float
class Unit: ...
@@ -52,7 +52,7 @@ class NonCallable:
__call__ = 1
a = NonCallable()
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown
```
@@ -67,8 +67,8 @@ def _(flag: bool):
def __call__(self) -> int: ...
a = NonCallable()
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(a()) # revealed: int | Unknown
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
```
## Call binding errors
@@ -99,26 +99,3 @@ c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```
## Union over callables
### Possibly unbound `__call__`
```py
def outer(cond1: bool):
class Test:
if cond1:
def __call__(self): ...
class Other:
def __call__(self): ...
def inner(cond2: bool):
if cond2:
a = Test()
else:
a = Other()
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
a()
```

View File

@@ -278,10 +278,10 @@ proper diagnostics in case of missing or superfluous arguments.
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type()
reveal_type() # revealed: Unknown
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2)
reveal_type(1, 2) # revealed: Literal[1]
```
### `static_assert`
@@ -290,6 +290,7 @@ reveal_type(1, 2)
from knot_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
# error: [static-assert-error]
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"

View File

@@ -1,133 +0,0 @@
# `inspect.getattr_static`
## Basic usage
`inspect.getattr_static` is a function that returns attributes of an object without invoking the
descriptor protocol (for caveats, see the [official documentation]).
Consider the following example:
```py
import inspect
class Descriptor:
def __get__(self, instance, owner) -> str:
return 1
class C:
normal: int = 1
descriptor: Descriptor = Descriptor()
```
If we access attributes on an instance of `C` as usual, the descriptor protocol is invoked, and we
get a type of `str` for the `descriptor` attribute:
```py
c = C()
reveal_type(c.normal) # revealed: int
reveal_type(c.descriptor) # revealed: str
```
However, if we use `inspect.getattr_static`, we can see the underlying `Descriptor` type:
```py
reveal_type(inspect.getattr_static(c, "normal")) # revealed: int
reveal_type(inspect.getattr_static(c, "descriptor")) # revealed: Descriptor
```
For non-existent attributes, a default value can be provided:
```py
reveal_type(inspect.getattr_static(C, "normal", "default-arg")) # revealed: int
reveal_type(inspect.getattr_static(C, "non_existent", "default-arg")) # revealed: Literal["default-arg"]
```
When a non-existent attribute is accessed without a default value, the runtime raises an
`AttributeError`. We could emit a diagnostic for this case, but that is currently not supported:
```py
# TODO: we could emit a diagnostic here
reveal_type(inspect.getattr_static(C, "non_existent")) # revealed: Never
```
We can access attributes on objects of all kinds:
```py
import sys
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1]
```
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
```py
class D:
def __init__(self) -> None:
self.instance_attr: int = 1
reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int
```
## Error cases
We can only infer precise types if the attribute is a literal string. In all other cases, we fall
back to `Any`:
```py
import inspect
class C:
x: int = 1
def _(attr_name: str):
reveal_type(inspect.getattr_static(C(), attr_name)) # revealed: Any
reveal_type(inspect.getattr_static(C(), attr_name, 1)) # revealed: Any
```
But we still detect errors in the number or type of arguments:
```py
# error: [missing-argument] "No arguments provided for required parameters `obj`, `attr` of function `getattr_static`"
inspect.getattr_static()
# error: [missing-argument] "No argument provided for required parameter `attr`"
inspect.getattr_static(C())
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`attr`) of function `getattr_static`; expected type `str`"
inspect.getattr_static(C(), 1)
# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
inspect.getattr_static(C(), "x", "default-arg", "one too many")
```
## Possibly unbound attributes
```py
import inspect
def _(flag: bool):
class C:
if flag:
x: int = 1
reveal_type(inspect.getattr_static(C, "x", "default")) # revealed: int | Literal["default"]
```
## Gradual types
```py
import inspect
from typing import Any
def _(a: Any, tuple_of_any: tuple[Any]):
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
# TODO: Ideally, this would just be `Literal[index]`
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"]
```
[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static

View File

@@ -1,258 +0,0 @@
# Methods
## Background: Functions as descriptors
> Note: See also this related section in the descriptor guide: [Functions and methods].
Say we have a simple class `C` with a function definition `f` inside its body:
```py
class C:
def f(self, x: int) -> str:
return "a"
```
Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance
(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors
because they implement a `__get__` method. This is crucial in making sure that method calls work as
expected. In general, the signature of the `__get__` method in the descriptor protocol is
`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The
passed value for the `instance` argument depends on whether the attribute is accessed from the class
object (in which case it is `None`), or from an instance (in which case it is the instance of type
`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize:
- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)`
- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)`
Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the
function attribute. The way the special `__get__` method *on functions* works is as follows. In the
former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In
the latter case, it returns a *bound method* object:
```py
from inspect import getattr_static
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `C`>
```
In conclusion, this is why we see the following two types when accessing the `f` attribute on the
class object `C` and on an instance `C()`:
```py
reveal_type(C.f) # revealed: Literal[f]
reveal_type(C().f) # revealed: <bound method `f` of `C`>
```
A bound method is a callable object that contains a reference to the `instance` that it was called
on (can be inspected via `__self__`), and the function object that it refers to (can be inspected
via `__func__`):
```py
bound_method = C().f
reveal_type(bound_method.__self__) # revealed: C
reveal_type(bound_method.__func__) # revealed: Literal[f]
```
When we call the bound method, the `instance` is implicitly passed as the first argument (`self`):
```py
reveal_type(C().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str
```
When we call the function object itself, we need to pass the `instance` explicitly:
```py
C.f(1) # error: [missing-argument]
reveal_type(C.f(C(), 1)) # revealed: str
```
When we access methods from derived classes, they will be bound to instances of the derived class:
```py
class D(C):
pass
reveal_type(D().f) # revealed: <bound method `f` of `D`>
```
If we access an attribute on a bound method object itself, it will defer to `types.MethodType`:
```py
reveal_type(bound_method.__hash__) # revealed: <bound method `__hash__` of `MethodType`>
```
If an attribute is not available on the bound method object, it will be looked up on the underlying
function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound
methods, even though it is not available on `types.MethodType`:
```py
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
```
## Basic method calls on class objects and instances
```py
class Base:
def method_on_base(self, x: int | None) -> str:
return "a"
class Derived(Base):
def method_on_derived(self, x: bytes) -> tuple[int, str]:
return (1, "a")
reveal_type(Base().method_on_base(1)) # revealed: str
reveal_type(Base.method_on_base(Base(), 1)) # revealed: str
Base().method_on_base("incorrect") # error: [invalid-argument-type]
Base().method_on_base() # error: [missing-argument]
Base().method_on_base(1, 2) # error: [too-many-positional-arguments]
reveal_type(Derived().method_on_base(1)) # revealed: str
reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str]
reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str
reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str]
```
## Method calls on literals
### Boolean literals
```py
reveal_type(True.bit_length()) # revealed: int
reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]]
```
### Integer literals
```py
reveal_type((42).bit_length()) # revealed: int
```
### String literals
```py
reveal_type("abcde".find("abc")) # revealed: int
reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes
"abcde".find(123) # error: [invalid-argument-type]
```
### Bytes literals
```py
reveal_type(b"abcde".startswith(b"abc")) # revealed: bool
```
## Method calls on `LiteralString`
```py
from typing_extensions import LiteralString
def f(s: LiteralString) -> None:
reveal_type(s.find("a")) # revealed: int
```
## Method calls on `tuple`
```py
def f(t: tuple[int, str]) -> None:
reveal_type(t.index("a")) # revealed: int
```
## Method calls on unions
```py
from typing import Any
class A:
def f(self) -> int:
return 1
class B:
def f(self) -> str:
return "a"
def f(a_or_b: A | B, any_or_a: Any | A):
reveal_type(a_or_b.f) # revealed: <bound method `f` of `A`> | <bound method `f` of `B`>
reveal_type(a_or_b.f()) # revealed: int | str
reveal_type(any_or_a.f) # revealed: Any | <bound method `f` of `A`>
reveal_type(any_or_a.f()) # revealed: Any | int
```
## Method calls on `KnownInstance` types
```toml
[environment]
python-version = "3.12"
```
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>
```
## Error cases: Calling `__get__` for methods
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
```py
from types import FunctionType, MethodType
from typing import overload
@overload
def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
@overload
def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
```
Here, we test that this signature is enforced correctly:
```py
from inspect import getattr_static
class C:
def f(self, x: int) -> str:
return "a"
method_wrapper = getattr_static(C, "f").__get__
reveal_type(method_wrapper) # revealed: <method-wrapper `__get__` of `f`>
# All of these are fine:
method_wrapper(C(), C)
method_wrapper(C())
method_wrapper(C(), None)
method_wrapper(None, C)
# Passing `None` without an `owner` argument is an
# error: [missing-argument] "No argument provided for required parameter `owner`"
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`); expected type `type`"
method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`); expected type `type`"
method_wrapper(None, None)
# Calling `__get__` without any arguments is an
# error: [missing-argument] "No argument provided for required parameter `instance`"
method_wrapper()
# Calling `__get__` with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments: expected 2, got 3"
method_wrapper(C(), C, "one too many")
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View File

@@ -39,8 +39,8 @@ def _(flag: bool):
else:
def f() -> int:
return 1
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(x) # revealed: int | Unknown
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
```
## Multiple non-callable elements in a union
@@ -56,8 +56,8 @@ def _(flag: bool, flag2: bool):
else:
def f() -> int:
return 1
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
# revealed: int | Unknown
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
@@ -72,39 +72,6 @@ def _(flag: bool):
else:
f = "foo"
x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```
## Mismatching signatures
Calling a union where the arguments don't match the signature of all variants.
```py
def f1(a: int) -> int: ...
def f2(a: str) -> str: ...
def _(flag: bool):
if flag:
f = f1
else:
f = f2
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
x = f(3)
reveal_type(x) # revealed: int | str
```
## Any non-callable variant
```py
def f1(a: int): ...
def _(flag: bool):
if flag:
f = f1
else:
f = "This is a string literal"
# error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable"
x = f(3)
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```

View File

@@ -21,9 +21,8 @@ class A:
reveal_type("hello" in A()) # revealed: bool
reveal_type("hello" not in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
# TODO: should emit diagnostic, need to check arg type, will fail
reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 not in A()) # revealed: bool
```
@@ -127,9 +126,9 @@ class A:
reveal_type(CheckContains() in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`"
# TODO: should emit diagnostic, need to check arg type,
# should not fall back to __iter__ or __getitem__
reveal_type(CheckIter() in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`"
reveal_type(CheckGetItem() in A()) # revealed: bool
class B:
@@ -155,8 +154,7 @@ class A:
def __getitem__(self, key: str) -> str:
return "foo"
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
# TODO should emit a diagnostic
reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
reveal_type("hello" in A()) # revealed: bool
```

View File

@@ -16,38 +16,31 @@ most common case involves implementing these methods for the same type:
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> EqReturnType:
return EqReturnType()
def __eq__(self, other: A) -> int:
return 42
def __ne__(self, other: A) -> NeReturnType:
return NeReturnType()
def __ne__(self, other: A) -> float:
return 42.0
def __lt__(self, other: A) -> LtReturnType:
return LtReturnType()
def __lt__(self, other: A) -> str:
return "42"
def __le__(self, other: A) -> LeReturnType:
return LeReturnType()
def __le__(self, other: A) -> bytes:
return b"42"
def __gt__(self, other: A) -> GtReturnType:
return GtReturnType()
def __gt__(self, other: A) -> list:
return [42]
def __ge__(self, other: A) -> GeReturnType:
return GeReturnType()
def __ge__(self, other: A) -> set:
return {42}
reveal_type(A() == A()) # revealed: EqReturnType
reveal_type(A() != A()) # revealed: NeReturnType
reveal_type(A() < A()) # revealed: LtReturnType
reveal_type(A() <= A()) # revealed: LeReturnType
reveal_type(A() > A()) # revealed: GtReturnType
reveal_type(A() >= A()) # revealed: GeReturnType
reveal_type(A() == A()) # revealed: int
reveal_type(A() != A()) # revealed: float
reveal_type(A() < A()) # revealed: str
reveal_type(A() <= A()) # revealed: bytes
reveal_type(A() > A()) # revealed: list
reveal_type(A() >= A()) # revealed: set
```
## Rich Comparison Dunder Implementations for Other Class
@@ -58,40 +51,33 @@ type:
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> EqReturnType:
return EqReturnType()
def __eq__(self, other: B) -> int:
return 42
def __ne__(self, other: B) -> NeReturnType:
return NeReturnType()
def __ne__(self, other: B) -> float:
return 42.0
def __lt__(self, other: B) -> LtReturnType:
return LtReturnType()
def __lt__(self, other: B) -> str:
return "42"
def __le__(self, other: B) -> LeReturnType:
return LeReturnType()
def __le__(self, other: B) -> bytes:
return b"42"
def __gt__(self, other: B) -> GtReturnType:
return GtReturnType()
def __gt__(self, other: B) -> list:
return [42]
def __ge__(self, other: B) -> GeReturnType:
return GeReturnType()
def __ge__(self, other: B) -> set:
return {42}
class B: ...
reveal_type(A() == B()) # revealed: EqReturnType
reveal_type(A() != B()) # revealed: NeReturnType
reveal_type(A() < B()) # revealed: LtReturnType
reveal_type(A() <= B()) # revealed: LeReturnType
reveal_type(A() > B()) # revealed: GtReturnType
reveal_type(A() >= B()) # revealed: GeReturnType
reveal_type(A() == B()) # revealed: int
reveal_type(A() != B()) # revealed: float
reveal_type(A() < B()) # revealed: str
reveal_type(A() <= B()) # revealed: bytes
reveal_type(A() > B()) # revealed: list
reveal_type(A() >= B()) # revealed: set
```
## Reflected Comparisons
@@ -103,64 +89,58 @@ these methods will be ignored here because they require a mismatched operand typ
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> EqReturnType:
return EqReturnType()
def __eq__(self, other: B) -> int:
return 42
def __ne__(self, other: B) -> NeReturnType:
return NeReturnType()
def __ne__(self, other: B) -> float:
return 42.0
def __lt__(self, other: B) -> LtReturnType:
return LtReturnType()
def __lt__(self, other: B) -> str:
return "42"
def __le__(self, other: B) -> LeReturnType:
return LeReturnType()
def __le__(self, other: B) -> bytes:
return b"42"
def __gt__(self, other: B) -> GtReturnType:
return GtReturnType()
def __gt__(self, other: B) -> list:
return [42]
def __ge__(self, other: B) -> GeReturnType:
return GeReturnType()
class Unrelated: ...
def __ge__(self, other: B) -> set:
return {42}
class B:
# To override builtins.object.__eq__ and builtins.object.__ne__
# TODO these should emit an invalid override diagnostic
def __eq__(self, other: Unrelated) -> B:
def __eq__(self, other: str) -> B:
return B()
def __ne__(self, other: Unrelated) -> B:
def __ne__(self, other: str) -> B:
return B()
# TODO: should be `int` and `float`.
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
#
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
# this can only happen with an invalid override of these methods,
# but we still support it.
reveal_type(B() == A()) # revealed: EqReturnType
reveal_type(B() != A()) # revealed: NeReturnType
reveal_type(B() == A()) # revealed: B
reveal_type(B() != A()) # revealed: B
reveal_type(B() < A()) # revealed: GtReturnType
reveal_type(B() <= A()) # revealed: GeReturnType
reveal_type(B() < A()) # revealed: list
reveal_type(B() <= A()) # revealed: set
reveal_type(B() > A()) # revealed: LtReturnType
reveal_type(B() >= A()) # revealed: LeReturnType
reveal_type(B() > A()) # revealed: str
reveal_type(B() >= A()) # revealed: bytes
class C:
def __gt__(self, other: C) -> EqReturnType:
def __gt__(self, other: C) -> int:
return 42
def __ge__(self, other: C) -> NeReturnType:
return NeReturnType()
def __ge__(self, other: C) -> float:
return 42.0
reveal_type(C() < C()) # revealed: EqReturnType
reveal_type(C() <= C()) # revealed: NeReturnType
reveal_type(C() < C()) # revealed: int
reveal_type(C() <= C()) # revealed: float
```
## Reflected Comparisons with Subclasses
@@ -172,13 +152,6 @@ than `A`.
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> A:
return A()
@@ -199,32 +172,32 @@ class A:
return A()
class B(A):
def __eq__(self, other: A) -> EqReturnType:
return EqReturnType()
def __eq__(self, other: A) -> int:
return 42
def __ne__(self, other: A) -> NeReturnType:
return NeReturnType()
def __ne__(self, other: A) -> float:
return 42.0
def __lt__(self, other: A) -> LtReturnType:
return LtReturnType()
def __lt__(self, other: A) -> str:
return "42"
def __le__(self, other: A) -> LeReturnType:
return LeReturnType()
def __le__(self, other: A) -> bytes:
return b"42"
def __gt__(self, other: A) -> GtReturnType:
return GtReturnType()
def __gt__(self, other: A) -> list:
return [42]
def __ge__(self, other: A) -> GeReturnType:
return GeReturnType()
def __ge__(self, other: A) -> set:
return {42}
reveal_type(A() == B()) # revealed: EqReturnType
reveal_type(A() != B()) # revealed: NeReturnType
reveal_type(A() == B()) # revealed: int
reveal_type(A() != B()) # revealed: float
reveal_type(A() < B()) # revealed: GtReturnType
reveal_type(A() <= B()) # revealed: GeReturnType
reveal_type(A() < B()) # revealed: list
reveal_type(A() <= B()) # revealed: set
reveal_type(A() > B()) # revealed: LtReturnType
reveal_type(A() >= B()) # revealed: LeReturnType
reveal_type(A() > B()) # revealed: str
reveal_type(A() >= B()) # revealed: bytes
```
## Reflected Comparisons with Subclass But Falls Back to LHS
@@ -249,8 +222,9 @@ class B(A):
def __gt__(self, other: int) -> B:
return B()
reveal_type(A() < B()) # revealed: A
reveal_type(A() > B()) # revealed: A
# TODO: should be `A`, need to check argument type and fall back to LHS method
reveal_type(A() < B()) # revealed: B
reveal_type(A() > B()) # revealed: B
```
## Operations involving instances of classes inheriting from `Any`
@@ -298,8 +272,9 @@ class A:
def __ne__(self, other: int) -> A:
return A()
reveal_type(A() == A()) # revealed: bool
reveal_type(A() != A()) # revealed: bool
# TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not`
reveal_type(A() == A()) # revealed: A
reveal_type(A() != A()) # revealed: A
```
## Object Comparisons with Typeshed
@@ -330,14 +305,12 @@ reveal_type(1 >= 1.0) # revealed: bool
reveal_type(1 == 2j) # revealed: bool
reveal_type(1 != 2j) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 < 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 <= 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 > 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 >= 2j) # revealed: Unknown
# TODO: should be Unknown and emit diagnostic,
# need to check arg type and should be failed
reveal_type(1 < 2j) # revealed: bool
reveal_type(1 <= 2j) # revealed: bool
reveal_type(1 > 2j) # revealed: bool
reveal_type(1 >= 2j) # revealed: bool
def f(x: bool, y: int):
reveal_type(x < y) # revealed: bool

View File

@@ -12,8 +12,8 @@ reveal_type(1 is 1) # revealed: bool
reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
# TODO: should be Unknown, and emit diagnostic, once we check call argument types
reveal_type(1 <= "" and 0 < 1) # revealed: bool
```
## Integer instance

View File

@@ -8,9 +8,7 @@ types, we can infer that the result for the intersection type is also true/false
```py
from typing import Literal
class Base:
def __gt__(self, other) -> bool:
return False
class Base: ...
class Child1(Base):
def __eq__(self, other) -> Literal[True]:

View File

@@ -23,7 +23,6 @@ from __future__ import annotations
class A:
def __lt__(self, other) -> A: ...
def __gt__(self, other) -> bool: ...
class B:
def __lt__(self, other) -> B: ...

View File

@@ -33,6 +33,8 @@ reveal_type(a >= b) # revealed: Literal[False]
Even when tuples have different lengths, comparisons should be handled appropriately.
`different_length.py`:
```py
a = (1, 2, 3)
b = (1, 2, 3, 4)
@@ -92,19 +94,18 @@ reveal_type(a == b) # revealed: bool
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
reveal_type(a != b) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a < b) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a <= b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a > b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a >= b) # revealed: Unknown
# TODO: should be Unknown and add more informative diagnostics
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
```
However, if the lexicographic comparison completes without reaching a point where str and int are
compared, Python will still produce a result based on the prior elements.
`short_circuit.py`:
```py
a = (1, 2)
b = (999999, "hello")
@@ -147,40 +148,33 @@ of the dunder methods.)
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, o: object) -> EqReturnType:
return EqReturnType()
def __eq__(self, o: object) -> str:
return "hello"
def __ne__(self, o: object) -> NeReturnType:
return NeReturnType()
def __ne__(self, o: object) -> bytes:
return b"world"
def __lt__(self, o: A) -> LtReturnType:
return LtReturnType()
def __lt__(self, o: A) -> float:
return 3.14
def __le__(self, o: A) -> LeReturnType:
return LeReturnType()
def __le__(self, o: A) -> complex:
return complex(0.5, -0.5)
def __gt__(self, o: A) -> GtReturnType:
return GtReturnType()
def __gt__(self, o: A) -> tuple:
return (1, 2, 3)
def __ge__(self, o: A) -> GeReturnType:
return GeReturnType()
def __ge__(self, o: A) -> list:
return [1, 2, 3]
a = (A(), A())
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: LtReturnType | Literal[False]
reveal_type(a <= a) # revealed: LeReturnType | Literal[True]
reveal_type(a > a) # revealed: GtReturnType | Literal[False]
reveal_type(a >= a) # revealed: GeReturnType | Literal[True]
reveal_type(a < a) # revealed: float | Literal[False]
reveal_type(a <= a) # revealed: complex | Literal[True]
reveal_type(a > a) # revealed: tuple | Literal[False]
reveal_type(a >= a) # revealed: list | Literal[True]
# If lexicographic comparison is finished before comparing A()
b = ("1_foo", A())
@@ -193,13 +187,11 @@ reveal_type(b <= c) # revealed: Literal[True]
reveal_type(b > c) # revealed: Literal[False]
reveal_type(b >= c) # revealed: Literal[False]
class LtReturnTypeOnB: ...
class B:
def __lt__(self, o: B) -> LtReturnTypeOnB:
def __lt__(self, o: B) -> set:
return set()
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
```
#### Special Handling of Eq and NotEq in Lexicographic Comparisons

View File

@@ -9,22 +9,28 @@ def _(flag: bool, flag1: bool, flag2: bool):
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `object` and `int`, in comparing `object` with `Literal[5]`"
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `object` and `int`")
c = object() < 5
reveal_type(c) # revealed: Unknown
# TODO: should be Unknown, once operand type check is implemented
reveal_type(c) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `object`, in comparing `Literal[5]` with `object`"
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `int` and `object`")
d = 5 < object()
reveal_type(d) # revealed: Unknown
# TODO: should be Unknown, once operand type check is implemented
reveal_type(d) # revealed: bool
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
f = (1, 2) < (1, "hello")
reveal_type(f) # revealed: Unknown
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (flag1, A()) < (flag2, A())

View File

@@ -43,7 +43,8 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# revealed: tuple[int, int]
# 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()]
```
@@ -66,7 +67,8 @@ class IterableOfIterables:
def __iter__(self) -> IteratorOfIterables:
return IteratorOfIterables()
# revealed: tuple[int, IntIterable]
# 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()]
```

View File

@@ -22,26 +22,22 @@ class Ten:
pass
class C:
ten: Ten = Ten()
ten = Ten()
c = C()
reveal_type(c.ten) # revealed: Literal[10]
# TODO: this should be `Literal[10]`
reveal_type(c.ten) # revealed: Unknown | Ten
reveal_type(C.ten) # revealed: Literal[10]
# TODO: This should `Literal[10]`
reveal_type(C.ten) # revealed: Unknown | Ten
# These are fine:
# TODO: This should not be an error
c.ten = 10 # error: [invalid-assignment]
c.ten = 10
C.ten = 10
# TODO: This should be an error (as the wrong type is being implicitly passed to `Ten.__set__`),
# but the error message is misleading.
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
# TODO: Both of these should be errors
c.ten = 11
# TODO: same as above
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
C.ten = 11
```
@@ -61,86 +57,24 @@ class FlexibleInt:
self._value = int(value)
class C:
flexible_int: FlexibleInt = FlexibleInt()
flexible_int = FlexibleInt()
c = C()
reveal_type(c.flexible_int) # revealed: int | None
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
# TODO: These should not be errors
# error: [invalid-assignment]
c.flexible_int = 42 # okay
# error: [invalid-assignment]
c.flexible_int = "42" # also okay!
reveal_type(c.flexible_int) # revealed: int | None
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
# TODO: This should be an error, but the message needs to be improved.
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`"
# TODO: should be an error
c.flexible_int = None # not okay
reveal_type(c.flexible_int) # revealed: int | None
```
## Data and non-data descriptors
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example\
of a data descriptor is a `property` with a setter and/or a deleter.\
Descriptors that only define `__get__`, meanwhile, are called *non-data descriptors*. Examples
include\
functions, `classmethod` or `staticmethod`).
The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3)
non-data descriptors.
```py
from typing import Literal
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: int, value) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
return "non-data"
class C:
data_descriptor = DataDescriptor()
non_data_descriptor = NonDataDescriptor()
def f(self):
# This explains why data descriptors come first in the precedence chain. If
# instance attributes would take priority, we would override the descriptor
# here. Instead, this calls `DataDescriptor.__set__`, i.e. it does not affect
# the type of the `data_descriptor` attribute.
self.data_descriptor = 1
# However, for non-data descriptors, instance attributes do take precedence.
# So it is possible to override them.
self.non_data_descriptor = 1
c = C()
# TODO: This should ideally be `Unknown | Literal["data"]`.
#
# - Pyright also wrongly shows `int | Literal['data']` here
# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below.
#
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1]
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"]
reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
# It is possible to override data descriptors via class objects. The following
# assignment does not call `DataDescriptor.__set__`. For this reason, we infer
# `Unknown | …` for all (descriptor) attributes.
C.data_descriptor = "something else" # This is okay
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
```
## Built-in `property` descriptor
@@ -167,7 +101,7 @@ c = C()
reveal_type(c._name) # revealed: str | None
# Should be `str`
reveal_type(c.name) # revealed: @Todo(decorated method)
reveal_type(c.name) # revealed: @Todo(bound method)
# Should be `builtins.property`
reveal_type(C.name) # revealed: Literal[name]
@@ -208,7 +142,7 @@ reveal_type(c1) # revealed: @Todo(return type)
reveal_type(C.get_name()) # revealed: @Todo(return type)
# TODO: should be `str`
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
```
## Descriptors only work when used as class variables
@@ -226,10 +160,9 @@ class Ten:
class C:
def __init__(self):
self.ten: Ten = Ten()
self.ten = Ten()
# TODO: Should be Ten
reveal_type(C().ten) # revealed: Literal[10]
reveal_type(C().ten) # revealed: Unknown | Ten
```
## Descriptors distinguishing between class and instance access
@@ -253,166 +186,13 @@ class Descriptor:
return "called on class object"
class C:
d: Descriptor = Descriptor()
d = Descriptor()
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: LiteralString
reveal_type(C.d) # revealed: Unknown | Descriptor
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: LiteralString
```
## Undeclared descriptor arguments
If a descriptor attribute is not declared, we union with `Unknown`, just like for regular
attributes, since that attribute could be overwritten externally. Even a data descriptor with a
`__set__` method can be overwritten when accessed through a class object.
```py
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: object, value: int) -> None:
pass
class C:
descriptor = Descriptor()
C.descriptor = "something else"
# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments
reveal_type(C.descriptor) # revealed: Unknown | int
```
## Descriptors with incorrect `__get__` signature
```py
class Descriptor:
# `__get__` method with missing parameters:
def __get__(self) -> int:
return 1
class C:
descriptor: Descriptor = Descriptor()
# TODO: This should be an error
reveal_type(C.descriptor) # revealed: Descriptor
```
## Possibly-unbound `__get__` method
```py
def _(flag: bool):
class MaybeDescriptor:
if flag:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
class C:
descriptor: MaybeDescriptor = MaybeDescriptor()
# TODO: This should be `MaybeDescriptor | int`
reveal_type(C.descriptor) # revealed: int
```
## Dunder methods
Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
```py
class SomeCallable:
def __call__(self, x: int) -> str:
return "a"
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
return SomeCallable()
class B:
__call__: Descriptor = Descriptor()
b_instance = B()
reveal_type(b_instance(1)) # revealed: str
b_instance("bla") # error: [invalid-argument-type]
```
## Functions as descriptors
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
that method calls work as expected. See [this test suite](./call/methods.md) for more information.
Here, we only demonstrate how `__get__` works on functions:
```py
from inspect import getattr_static
def f(x: object) -> str:
return "a"
reveal_type(f) # revealed: Literal[f]
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(f.__get__(None, type(f))) # revealed: Literal[f]
reveal_type(f.__get__(None, type(f))(1)) # revealed: str
wrapper_descriptor = getattr_static(f, "__get__")
reveal_type(wrapper_descriptor) # revealed: <wrapper-descriptor `__get__` of `function` objects>
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`:
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property)
```
We can also bind the free function `f` to an instance of a class `C`:
```py
class C: ...
bound_method = wrapper_descriptor(f, C(), C)
reveal_type(bound_method) # revealed: <bound method `f` of `C`>
```
We can then call it, and the instance of `C` is implicitly passed to the first parameter of `f`
(`x`):
```py
reveal_type(bound_method()) # revealed: str
```
Finally, we test some error cases for the call to the wrapper descriptor:
```py
# Calling the wrapper descriptor without any arguments is an
# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`"
wrapper_descriptor()
# Calling it without the `instance` argument is an also an
# error: [missing-argument] "No argument provided for required parameter `instance`"
wrapper_descriptor(f)
# Calling it without the `owner` argument if `instance` is not `None` is an
# error: [missing-argument] "No argument provided for required parameter `owner`"
wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
wrapper_descriptor(f, C())
# Calling it with something that is not a `FunctionType` as the first argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`); expected type `FunctionType`"
wrapper_descriptor(1, None, type(f))
# Calling it with something that is not a `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`); expected type `type`"
wrapper_descriptor(f, None, f)
# Calling it with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments: expected 3, got 4"
wrapper_descriptor(f, None, type(f), "one too many")
reveal_type(C().d) # revealed: Unknown | Descriptor
```
[descriptors]: https://docs.python.org/3/howto/descriptor.html

View File

@@ -1,184 +0,0 @@
# Invalid argument type diagnostics
<!-- snapshot-diagnostics -->
## Basic
This is a basic test demonstrating that a diagnostic points to the function definition corresponding
to the invalid argument.
```py
def foo(x: int) -> int:
return x * x
foo("hello") # error: [invalid-argument-type]
```
## Different source order
This is like the basic test, except we put the call site above the function definition.
```py
def bar():
foo("hello") # error: [invalid-argument-type]
def foo(x: int) -> int:
return x * x
```
## Different files
This tests that a diagnostic can point to a function definition in a different file in which an
invalid call site was found.
`package.py`:
```py
def foo(x: int) -> int:
return x * x
```
```py
import package
package.foo("hello") # error: [invalid-argument-type]
```
## Many parameters
This checks that a diagnostic renders reasonably when there are multiple parameters.
```py
def foo(x: int, y: int, z: int) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
## Many parameters across multiple lines
This checks that a diagnostic renders reasonably when there are multiple parameters spread out
across multiple lines.
```py
def foo(
x: int,
y: int,
z: int,
) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
## Many parameters with multiple invalid arguments
This checks that a diagnostic renders reasonably when there are multiple parameters and multiple
invalid argument types.
```py
def foo(x: int, y: int, z: int) -> int:
return x * y * z
# error: [invalid-argument-type]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
foo("a", "b", "c")
```
At present (2025-02-18), this renders three different diagnostic messages. But arguably, these could
all be folded into one diagnostic. Fixing this requires at least better support for multi-spans in
the diagnostic model and possibly also how diagnostics are emitted by the type checker itself.
## Test calling a function whose type is vendored from `typeshed`
This tests that diagnostic rendering is reasonable when the function being called is from the
standard library.
```py
import json
json.loads(5) # error: [invalid-argument-type]
```
## Tests for a variety of argument types
These tests check that diagnostic output is reasonable regardless of the kinds of arguments used in
a function definition.
### Only positional
Tests a function definition with only positional parameters.
```py
def foo(x: int, y: int, z: int, /) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
### Variadic arguments
Tests a function definition with variadic arguments.
```py
def foo(*numbers: int) -> int:
return len(numbers)
foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
```
### Keyword only arguments
Tests a function definition with keyword-only arguments.
```py
def foo(x: int, y: int, *, z: int = 0) -> int:
return x * y * z
foo(1, 2, z="hello") # error: [invalid-argument-type]
```
### One keyword argument
Tests a function definition with keyword-only arguments.
```py
def foo(x: int, y: int, z: int = 0) -> int:
return x * y * z
foo(1, 2, "hello") # error: [invalid-argument-type]
```
### Variadic keyword arguments
```py
def foo(**numbers: int) -> int:
return len(numbers)
foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
```
### Mix of arguments
Tests a function definition with multiple different kinds of arguments.
```py
def foo(x: int, /, y: int, *, z: int = 0) -> int:
return x * y * z
foo(1, 2, z="hello") # error: [invalid-argument-type]
```
### Synthetic arguments
Tests a function call with synthetic arguments.
```py
class C:
def __call__(self, x: int) -> int:
return 1
c = C()
c("wrong") # error: [invalid-argument-type]
```

View File

@@ -1,21 +0,0 @@
# Unpacking
<!-- snapshot-diagnostics -->
## Right hand side not iterable
```py
a, b = 1 # error: [not-iterable]
```
## Too many values to unpack
```py
a, b = (1, 2, 3) # error: [invalid-assignment]
```
## Too few values to unpack
```py
a, b = (1,) # error: [invalid-assignment]
```

View File

@@ -1,2 +0,0 @@
This directory contains user-facing documentation, but also doubles as an extended test suite that
makes sure that our documentation stays up to date.

View File

@@ -1,125 +0,0 @@
# Public type of undeclared symbols
## Summary
One major deviation from the behavior of existing Python type checkers is our handling of 'public'
types for undeclared symbols. This is best illustrated with an example:
```py
class Wrapper:
value = None
wrapper = Wrapper()
reveal_type(wrapper.value) # revealed: Unknown | None
wrapper.value = 1
```
Mypy and Pyright both infer a type of `None` for the type of `wrapper.value`. Consequently, both
tools emit an error when trying to assign `1` to `wrapper.value`. But there is nothing wrong with
this program. Emitting an error here violates the [gradual guarantee] which states that *"Removing
type annotations (making the program more dynamic) should not result in additional static type
errors."*: If `value` were annotated with `int | None` here, Mypy and Pyright would not emit any
errors.
By inferring `Unknown | None` instead, we allow arbitrary values to be assigned to `wrapper.value`.
This is a deliberate choice to prevent false positive errors on untyped code.
More generally, we infer `Unknown | T_inferred` for undeclared symbols, where `T_inferred` is the
inferred type of the right-hand side of the assignment. This gradual type represents an *unknown*
fully-static type that is *at least as large as* `T_inferred`. It accurately describes our static
knowledge about this type. In the example above, we don't know what values `wrapper.value` could
possibly contain, but we *do know* that `None` is a possibility. This allows us to catch errors
where `wrapper.value` is used in a way that is incompatible with `None`:
```py
def accepts_int(i: int) -> None:
pass
def f(w: Wrapper) -> None:
# This is fine
v: int | None = w.value
# This function call is incorrect, because `w.value` could be `None`. We therefore emit the following
# error: "`Unknown | None` cannot be assigned to parameter 1 (`i`) of function `accepts_int`; expected type `int`"
c = accepts_int(w.value)
```
## Explicit lack of knowledge
The following example demonstrates how Mypy and Pyright's type inference of fully-static types in
these situations can lead to false-negatives, even though everything appears to be (statically)
typed. To make this a bit more realistic, imagine that `OptionalInt` is imported from an external,
untyped module:
`optional_int.py`:
```py
class OptionalInt:
value = 10
def reset(o):
o.value = None
```
It is then used like this:
```py
from optional_int import OptionalInt, reset
o = OptionalInt()
reset(o) # Oh no...
# Mypy and Pyright infer a fully-static type of `int` here, which appears to make the
# subsequent division operation safe -- but it is not. We infer the following type:
reveal_type(o.value) # revealed: Unknown | Literal[10]
print(o.value // 2) # Runtime error!
```
We do not catch this mistake either, but we accurately reflect our lack of knowledge about
`o.value`. Together with a possible future type-checker mode that would detect the prevalence of
dynamic types, this could help developers catch such mistakes.
## Stricter behavior
Users can always opt in to stricter behavior by adding type annotations. For the `OptionalInt`
class, this would probably be:
```py
class OptionalInt:
value: int | None = 10
o = OptionalInt()
# The following public type is now
# revealed: int | None
reveal_type(o.value)
# Incompatible assignments are now caught:
# error: "Object of type `Literal["a"]` is not assignable to attribute `value` of type `int | None`"
o.value = "a"
```
## What is meant by 'public' type?
We apply different semantics depending on whether a symbol is accessed from the same scope in which
it was originally defined, or whether it is accessed from an external scope. External scopes will
see the symbol's "public type", which has been discussed above. But within the same scope the symbol
was defined in, we use a narrower type of `T_inferred` for undeclared symbols. This is because, from
the perspective of this scope, there is no way that the value of the symbol could have been
reassigned from external scopes. For example:
```py
class Wrapper:
value = None
# Type as seen from the same scope:
reveal_type(value) # revealed: None
# Type as seen from another scope:
reveal_type(Wrapper.value) # revealed: Unknown | None
```
[gradual guarantee]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-gradual-guarantee

View File

@@ -124,49 +124,42 @@ def _(e: Exception | type[Exception] | None):
## Exception cause is not an exception
```py
def _():
try:
raise EOFError() from GeneratorExit # fine
except:
...
try:
raise EOFError() from GeneratorExit # fine
except:
...
def _():
try:
raise StopIteration from MemoryError() # fine
except:
...
try:
raise StopIteration from MemoryError() # fine
except:
...
def _():
try:
raise BufferError() from None # fine
except:
...
try:
raise BufferError() from None # fine
except:
...
def _():
try:
raise ZeroDivisionError from False # error: [invalid-raise]
except:
...
try:
raise ZeroDivisionError from False # error: [invalid-raise]
except:
...
def _():
try:
raise SystemExit from bool() # error: [invalid-raise]
except:
...
try:
raise SystemExit from bool() # error: [invalid-raise]
except:
...
def _():
try:
raise
except KeyboardInterrupt as e: # fine
reveal_type(e) # revealed: KeyboardInterrupt
raise LookupError from e # fine
try:
raise
except KeyboardInterrupt as e: # fine
reveal_type(e) # revealed: KeyboardInterrupt
raise LookupError from e # fine
def _():
try:
raise
except int as e: # error: [invalid-exception-caught]
reveal_type(e) # revealed: Unknown
raise KeyError from e
try:
raise
except int as e: # error: [invalid-exception-caught]
reveal_type(e) # revealed: Unknown
raise KeyError from e
def _(e: Exception | type[Exception]):
raise ModuleNotFoundError from e # fine

View File

@@ -29,6 +29,8 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
*after* that redefinition.
`union_type_inferred.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -50,7 +52,12 @@ reveal_type(x) # revealed: str | Literal[2]
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
inferred as having a union type following the `try`/`except` block:
`branches_unify_to_non_union_type.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -130,6 +137,8 @@ the `except` suite:
- At the end of `else`, `x == 3`
- At the end of `except`, `x == 2`
`single_except.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -158,6 +167,9 @@ been executed in its entirety, or the `try` suite and the `else` suite must both
in their entireties:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -186,6 +198,8 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
type of `x` at the end of the example is therefore `Literal[2]`:
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -211,7 +225,12 @@ at this point than there were when we were inside the `finally` block.
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
still a TODO item for us.)
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -240,35 +259,33 @@ suites:
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
`except` suite ran to completion
`redef_in_finally.py`:
```py
class A: ...
class B: ...
class C: ...
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_bool() -> bool:
return True
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_A()
reveal_type(x) # revealed: A
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
finally:
# TODO: should be `Literal[1] | A | B | C`
reveal_type(x) # revealed: A | C
# TODO: should be `Literal[1] | str | bytes | bool`
reveal_type(x) # revealed: str | bool
x = 2
reveal_type(x) # revealed: Literal[2]
@@ -281,61 +298,80 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
suite.)
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_A()
reveal_type(x) # revealed: A
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
finally:
# TODO: should be `Literal[1] | A | B | C`
reveal_type(x) # revealed: A | C
# TODO: should be `Literal[1] | str | bytes | bool`
reveal_type(x) # revealed: str | bool
reveal_type(x) # revealed: A | C
reveal_type(x) # revealed: str | bool
```
An example with multiple `except` branches and a `finally` branch:
`multiple_except_branches.py`:
```py
class D: ...
class E: ...
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_E() -> E:
return E()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_A()
reveal_type(x) # revealed: A
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
except ValueError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `Literal[1] | A | B | C | D | E`
reveal_type(x) # revealed: A | C | E
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: str | bool | float
reveal_type(x) # revealed: A | C | E
reveal_type(x) # revealed: str | bool | float
```
## Combining `except`, `else` and `finally` branches
@@ -344,94 +380,104 @@ If the exception handler has an `else` branch, we must also take into account th
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
an exception raised *there*.
`single_except_branch.py`:
```py
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_E() -> E:
return E()
def could_raise_returns_float() -> float:
return 3.14
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_A()
reveal_type(x) # revealed: A
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
else:
reveal_type(x) # revealed: A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
reveal_type(x) # revealed: str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `Literal[1] | A | B | C | D | E`
reveal_type(x) # revealed: C | E
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: bool | float
reveal_type(x) # revealed: C | E
reveal_type(x) # revealed: bool | float
```
The same again, this time with multiple `except` branches:
`multiple_except_branches.py`:
```py
class F: ...
class G: ...
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_F() -> F:
return F()
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_G() -> G:
return G()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_slice() -> slice:
return slice(None)
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_A()
reveal_type(x) # revealed: A
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
except ValueError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
else:
reveal_type(x) # revealed: A
x = could_raise_returns_F()
reveal_type(x) # revealed: F
x = could_raise_returns_G()
reveal_type(x) # revealed: G
reveal_type(x) # revealed: str
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
finally:
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
reveal_type(x) # revealed: C | E | G
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
reveal_type(x) # revealed: C | E | G
reveal_type(x) # revealed: bool | float | slice
```
## Nested `try`/`except` blocks
@@ -445,101 +491,92 @@ a suite containing statements that could possibly raise exceptions, which would
jumping out of that suite prior to the suite running to completion.
```py
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
class F: ...
class G: ...
class H: ...
class I: ...
class J: ...
class K: ...
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_E() -> E:
return E()
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_F() -> F:
return F()
def could_raise_returns_slice() -> slice:
return slice(None)
def could_raise_returns_G() -> G:
return G()
def could_raise_returns_complex() -> complex:
return 3j
def could_raise_returns_H() -> H:
return H()
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_I() -> I:
return I()
class Foo: ...
class Bar: ...
def could_raise_returns_J() -> J:
return J()
def could_raise_returns_Foo() -> Foo:
return Foo()
def could_raise_returns_K() -> K:
return K()
def could_raise_returns_Bar() -> Bar:
return Bar()
x = 1
try:
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_A()
reveal_type(x) # revealed: A
x = could_raise_returns_str()
reveal_type(x) # revealed: str
except TypeError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
except ValueError:
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
else:
reveal_type(x) # revealed: A
x = could_raise_returns_F()
reveal_type(x) # revealed: F
x = could_raise_returns_G()
reveal_type(x) # revealed: G
reveal_type(x) # revealed: str
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
finally:
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
reveal_type(x) # revealed: C | E | G
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: Literal[2]
except:
reveal_type(x) # revealed: Literal[1, 2] | A | B | C | D | E | F | G
x = could_raise_returns_H()
reveal_type(x) # revealed: H
x = could_raise_returns_I()
reveal_type(x) # revealed: I
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice
x = could_raise_returns_complex()
reveal_type(x) # revealed: complex
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
else:
reveal_type(x) # revealed: Literal[2]
x = could_raise_returns_J()
reveal_type(x) # revealed: J
x = could_raise_returns_K()
reveal_type(x) # revealed: K
x = could_raise_returns_Foo()
reveal_type(x) # revealed: Foo
x = could_raise_returns_Bar()
reveal_type(x) # revealed: Bar
finally:
# TODO: should be `Literal[1, 2] | A | B | C | D | E | F | G | H | I | J | K`
reveal_type(x) # revealed: I | K
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice | complex | bytearray | Foo | Bar`
reveal_type(x) # revealed: bytearray | Bar
# Either one `except` branch or the `else`
# must have been taken and completed to get here:
reveal_type(x) # revealed: I | K
reveal_type(x) # revealed: bytearray | Bar
```
## Nested scopes inside `try` blocks
@@ -548,56 +585,50 @@ Shadowing a variable in an inner scope has no effect on type inference of the va
in the outer scope:
```py
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_E() -> E:
return E()
def could_raise_returns_float() -> float:
return 3.14
x = 1
try:
def foo(param=could_raise_returns_A()):
x = could_raise_returns_A()
def foo(param=could_raise_returns_str()):
x = could_raise_returns_str()
try:
reveal_type(x) # revealed: A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
reveal_type(x) # revealed: str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
except:
reveal_type(x) # revealed: A | B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
x = could_raise_returns_D()
reveal_type(x) # revealed: D
reveal_type(x) # revealed: str | bytes
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `A | B | C | D`
reveal_type(x) # revealed: B | D
reveal_type(x) # revealed: B | D
# TODO: should be `str | bytes | bytearray | float`
reveal_type(x) # revealed: bytes | float
reveal_type(x) # revealed: bytes | float
x = foo
reveal_type(x) # revealed: Literal[foo]
except:
reveal_type(x) # revealed: Literal[1] | Literal[foo]
class Bar:
x = could_raise_returns_E()
reveal_type(x) # revealed: E
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = Bar
reveal_type(x) # revealed: Literal[Bar]

View File

@@ -1,371 +0,0 @@
# Import conventions
This document describes the conventions for importing symbols.
Reference:
- <https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions>
## Builtins scope
When looking up for a name, red knot will fallback to using the builtins scope if the name is not
found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the
builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not
re-exported.
```py
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
# available in the builtins scope.
# error: "Name `Literal` used when not defined"
reveal_type(Literal) # revealed: Unknown
# error: "Name `sys` used when not defined"
reveal_type(sys) # revealed: Unknown
```
## Builtins import
Similarly, trying to import the symbols from the builtins module which aren't re-exported should
also raise an error.
```py
# error: "Module `builtins` has no member `Literal`"
# error: "Module `builtins` has no member `sys`"
from builtins import Literal, sys
reveal_type(Literal) # revealed: Unknown
reveal_type(sys) # revealed: Unknown
# error: "Module `math` has no member `Iterable`"
from math import Iterable
reveal_type(Iterable) # revealed: Unknown
```
## Re-exported symbols in stub files
When a symbol is re-exported, importing it should not raise an error. This tests both `import ...`
and `from ... import ...` forms.
Note: Submodule imports in `import ...` form doesn't work because it's a syntax error. For example,
in `import os.path as os.path` the `os.path` is not a valid identifier.
```py
from b import Any, Literal, foo
reveal_type(Any) # revealed: typing.Any
reveal_type(Literal) # revealed: typing.Literal
reveal_type(foo) # revealed: <module 'foo'>
```
`b.pyi`:
```pyi
import foo as foo
from typing import Any as Any, Literal as Literal
```
`foo.py`:
```py
```
## Non-exported symbols in stub files
Here, none of the symbols are being re-exported in the stub file.
```py
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
```
`b.pyi`:
```pyi
import foo
from typing import Any, Literal
```
`foo.pyi`:
```pyi
```
## Nested non-exports
Here, a chain of modules all don't re-export an import.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
# error: "Module `b` has no member `Any`"
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Nested mixed re-export and not
But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not
raise an error at that step in the chain.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any as Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Exported as different name
The re-export convention only works when the aliased name is exactly the same as the original name.
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import AnyFoo as Foo
reveal_type(Foo) # revealed: Literal[AnyFoo]
```
`b.pyi`:
```pyi
class AnyFoo: ...
```
## Exported using `__all__`
Here, the symbol is re-exported using the `__all__` variable.
```py
# TODO: This should *not* be an error but we don't understand `__all__` yet.
# error: "Module `a` has no member `Foo`"
from a import Foo
```
`a.pyi`:
```pyi
from b import Foo
__all__ = ['Foo']
```
`b.pyi`:
```pyi
class Foo: ...
```
## Re-exports in `__init__.pyi`
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
but the inference would be `Unknown`.
```py
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
```
`a/__init__.pyi`:
```pyi
from .b import c
from .foo import Foo
```
`a/foo.pyi`:
```pyi
class Foo: ...
```
`a/b/__init__.pyi`:
```pyi
```
`a/b/c.pyi`:
```pyi
```
## Conditional re-export in stub file
The following scenarios are when a re-export happens conditionally in a stub file.
### Global import
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: str
```
`a.pyi`:
```pyi
from b import Foo
def coinflip() -> bool: ...
if coinflip():
Foo: str = ...
reveal_type(Foo) # revealed: Literal[Foo] | str
```
`b.pyi`:
```pyi
class Foo: ...
```
### Both branch is an import
Here, both the branches of the condition are import statements where one of them re-exports while
the other does not.
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
else:
from b import Foo as Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`b.pyi`:
```pyi
class Foo: ...
```
### Re-export in one branch
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo as Foo
```
`b.pyi`:
```pyi
class Foo: ...
```
### Non-export in one branch
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
```
`b.pyi`:
```pyi
class Foo: ...
```

View File

@@ -218,21 +218,3 @@ import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```
## Relative imports at the top of a search path
Relative imports at the top of a search path result in a runtime error:
`ImportError: attempted relative import with no known parent package`. That's why Red Knot should
disallow them.
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from .parser import X # error: [unresolved-import]
```

View File

@@ -183,32 +183,25 @@ for x in Test():
## Union type as iterable and union type as iterator
```py
class Result1A: ...
class Result1B: ...
class Result2A: ...
class Result2B: ...
class Result3: ...
class Result4: ...
class TestIter1:
def __next__(self) -> Result1A | Result1B:
return Result1B()
class TestIter:
def __next__(self) -> int | Exception:
return 42
class TestIter2:
def __next__(self) -> Result2A | Result2B:
return Result2B()
def __next__(self) -> str | tuple[int, int]:
return "42"
class TestIter3:
def __next__(self) -> Result3:
return Result3()
def __next__(self) -> bytes:
return b"42"
class TestIter4:
def __next__(self) -> Result4:
return Result4()
def __next__(self) -> memoryview:
return memoryview(b"42")
class Test:
def __iter__(self) -> TestIter1 | TestIter2:
return TestIter1()
def __iter__(self) -> TestIter | TestIter2:
return TestIter()
class Test2:
def __iter__(self) -> TestIter3 | TestIter4:
@@ -216,7 +209,7 @@ class Test2:
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B | Result3 | Result4
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
```
## Union type as iterable where one union element has no `__iter__` method
@@ -252,10 +245,9 @@ class Test2:
return 42
def _(flag: bool):
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
reveal_type(x) # revealed: Unknown
```
## Union type as iterator where one union element has no `__next__` method
@@ -271,5 +263,5 @@ class Test:
# error: [not-iterable] "Object of type `Test` is not iterable"
for x in Test():
reveal_type(x) # revealed: int
reveal_type(x) # revealed: Unknown
```

View File

@@ -64,39 +64,3 @@ def _(flag1: bool, flag2: bool):
else:
reveal_type(x) # revealed: Literal[1]
```
## `is` for `EllipsisType` (Python 3.10+)
```toml
[environment]
python-version = "3.10"
```
```py
from types import EllipsisType
def _(x: int | EllipsisType):
if x is ...:
reveal_type(x) # revealed: EllipsisType
else:
reveal_type(x) # revealed: int
```
## `is` for `EllipsisType` (Python 3.9 and below)
```toml
[environment]
python-version = "3.9"
```
```py
def _(flag: bool):
x = ... if flag else 42
reveal_type(x) # revealed: ellipsis | Literal[42]
if x is ...:
reveal_type(x) # revealed: ellipsis
else:
reveal_type(x) # revealed: Literal[42]
```

View File

@@ -1,15 +0,0 @@
# Protocols
We do not support protocols yet, but to avoid false positives, we *partially* support some known
protocols.
## `typing.SupportsIndex`
```py
from typing import SupportsIndex, Literal
def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex):
a: SupportsIndex = some_int
b: SupportsIndex = some_literal_int
c: SupportsIndex = some_indexable
```

View File

@@ -1,382 +0,0 @@
# Eager scopes
Some scopes are executed eagerly: references to variables defined in enclosing scopes are resolved
_immediately_. This is in constrast to (for instance) function scopes, where those references are
resolved when the function is called.
## Function definitions
Function definitions are evaluated lazily.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[2]
x = 2
```
## Class definitions
Class definitions are evaluated eagerly.
```py
def _():
x = 1
class A:
reveal_type(x) # revealed: Literal[1]
y = x
x = 2
reveal_type(A.y) # revealed: Unknown | Literal[1]
```
## List comprehensions
List comprehensions are evaluated eagerly.
```py
def _():
x = 1
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
## Set comprehensions
Set comprehensions are evaluated eagerly.
```py
def _():
x = 1
# revealed: Literal[1]
{reveal_type(x) for a in range(1)}
x = 2
```
## Dict comprehensions
Dict comprehensions are evaluated eagerly.
```py
def _():
x = 1
# revealed: Literal[1]
{a: reveal_type(x) for a in range(1)}
x = 2
```
## Generator expressions
Generator expressions don't necessarily run eagerly, but in practice usually they do, so assuming
they do is the better default.
```py
def _():
x = 1
# revealed: Literal[1]
list(reveal_type(x) for a in range(1))
x = 2
```
But that does lead to incorrect results when the generator expression isn't run immediately:
```py
def evaluated_later():
x = 1
# revealed: Literal[1]
y = (reveal_type(x) for a in range(1))
x = 2
# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
# our inferred type.
print(next(y))
```
Though note that “the iterable expression in the leftmost `for` clause is immediately evaluated”
\[[spec][generators]\]:
```py
def iterable_evaluated_eagerly():
x = 1
# revealed: Literal[1]
y = (a for a in [reveal_type(x)])
x = 2
# Even though the generator isn't evaluated until here, the first iterable was evaluated
# immediately, so our inferred type is correct.
print(next(y))
```
## Top-level eager scopes
All of the above examples behave identically when the eager scopes are directly nested in the global
scope.
### Class definitions
```py
x = 1
class A:
reveal_type(x) # revealed: Literal[1]
y = x
x = 2
reveal_type(A.y) # revealed: Unknown | Literal[1]
```
### List comprehensions
```py
x = 1
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
### Set comprehensions
```py
x = 1
# revealed: Literal[1]
{reveal_type(x) for a in range(1)}
x = 2
```
### Dict comprehensions
```py
x = 1
# revealed: Literal[1]
{a: reveal_type(x) for a in range(1)}
x = 2
```
### Generator expressions
```py
x = 1
# revealed: Literal[1]
list(reveal_type(x) for a in range(1))
x = 2
```
`evaluated_later.py`:
```py
x = 1
# revealed: Literal[1]
y = (reveal_type(x) for a in range(1))
x = 2
# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
# our inferred type.
print(next(y))
```
`iterable_evaluated_eagerly.py`:
```py
x = 1
# revealed: Literal[1]
y = (a for a in [reveal_type(x)])
x = 2
# Even though the generator isn't evaluated until here, the first iterable was evaluated
# immediately, so our inferred type is correct.
print(next(y))
```
## Lazy scopes are "sticky"
As we look through each enclosing scope when resolving a reference, lookups become lazy as soon as
we encounter any lazy scope, even if there are other eager scopes that enclose it.
### Eager scope within eager scope
If we don't encounter a lazy scope, lookup remains eager. The resolved binding is not necessarily in
the immediately enclosing scope. Here, the list comprehension and class definition are both eager
scopes, and we immediately resolve the use of `x` to (only) the `x = 1` binding.
```py
def _():
x = 1
class A:
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
### Class definition bindings are not visible in nested scopes
Class definitions are eager scopes, but any bindings in them are explicitly not visible to any
nested scopes. (Those nested scopes are typically (lazy) function definitions, but the rule also
applies to nested eager scopes like comprehensions and other class definitions.)
```py
def _():
x = 1
class A:
x = 4
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
class B:
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
### Eager scope within a lazy scope
The list comprehension is an eager scope, and it is enclosed within a function definition, which is
a lazy scope. Because we pass through this lazy scope before encountering any bindings or
definitions, the lookup is lazy.
```py
def _():
x = 1
def f():
# revealed: Unknown | Literal[2]
[reveal_type(x) for a in range(1)]
x = 2
```
### Lazy scope within an eager scope
The function definition is a lazy scope, and it is enclosed within a class definition, which is an
eager scope. Even though we pass through an eager scope before encountering any bindings or
definitions, the lookup remains lazy.
```py
def _():
x = 1
class A:
def f():
# revealed: Unknown | Literal[2]
reveal_type(x)
x = 2
```
### Lazy scope within a lazy scope
No matter how many lazy scopes we pass through before encountering a binding or definition, the
lookup remains lazy.
```py
def _():
x = 1
def f():
def g():
# revealed: Unknown | Literal[2]
reveal_type(x)
x = 2
```
### Eager scope within a lazy scope within another eager scope
We have a list comprehension (eager scope), enclosed within a function definition (lazy scope),
enclosed within a class definition (eager scope), all of which we must pass through before
encountering any binding of `x`. Even though the last scope we pass through is eager, the lookup is
lazy, since we encountered a lazy scope on the way.
```py
def _():
x = 1
class A:
def f():
# revealed: Unknown | Literal[2]
[reveal_type(x) for a in range(1)]
x = 2
```
## Annotations
Type annotations are sometimes deferred. When they are, the types that are referenced in an
annotation are looked up lazily, even if they occur in an eager scope.
### Eager annotations in a Python file
```py
x = int
class C:
var: x
reveal_type(C.var) # revealed: int
x = str
```
### Deferred annotations in a Python file
```py
from __future__ import annotations
x = int
class C:
var: x
reveal_type(C.var) # revealed: Unknown | str
x = str
```
### Deferred annotations in a stub file
```pyi
x = int
class C:
var: x
reveal_type(C.var) # revealed: Unknown | str
x = str
```
[generators]: https://docs.python.org/3/reference/expressions.html#generator-expressions

View File

@@ -9,7 +9,7 @@ is unbound.
```py
reveal_type(__name__) # revealed: str
reveal_type(__file__) # revealed: str | None
reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None
reveal_type(__loader__) # revealed: LoaderProtocol | None
reveal_type(__package__) # revealed: str | None
reveal_type(__doc__) # revealed: str | None
@@ -29,6 +29,8 @@ def foo():
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
are excluded:
`unbound_dunders.py`:
```py
# error: [unresolved-reference]
# revealed: Unknown
@@ -54,10 +56,10 @@ inside the module:
import typing
reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: <bound method `__init__` of `ModuleType`>
reveal_type(typing.__init__) # revealed: @Todo(bound method)
# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
@@ -70,7 +72,11 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
dynamic imports; but we ignore that for module-literal types where we know exactly which module
we're dealing with:
`__getattr__.py`:
```py
import typing
# error: [unresolved-attribute]
reveal_type(typing.__getattr__) # revealed: Unknown
```

View File

@@ -5,6 +5,8 @@
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
No diagnostics should be generated.
`a.py`:
```py
def f(x: str):
x: int = int(x)
@@ -12,6 +14,8 @@ def f(x: str):
## Implicit error
`a.py`:
```py
def f(): ...
@@ -20,6 +24,8 @@ f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explici
## Explicit shadowing
`a.py`:
```py
def f(): ...

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
@@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
--> /src/mdtest_snippet__1.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | # Topmost component resolvable, submodule not resolvable:
@@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:2:8
--> /src/mdtest_snippet__1.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
@@ -41,7 +41,7 @@ error: lint:unresolved-import
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:5:8
--> /src/mdtest_snippet__1.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int) -> int:
2 | return x * x
3 |
4 | foo("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:5
|
2 | return x * x
3 |
4 | foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * x
|
```

View File

@@ -1,45 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## package.py
```
1 | def foo(x: int) -> int:
2 | return x * x
```
## mdtest_snippet.py
```
1 | import package
2 |
3 | package.foo("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:3:13
|
1 | import package
2 |
3 | package.foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/package.py:1:9
|
1 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * x
|
```

View File

@@ -1,43 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def bar():
2 | foo("hello") # error: [invalid-argument-type]
3 |
4 | def foo(x: int) -> int:
5 | return x * x
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:2:9
|
1 | def bar():
2 | foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
3 |
4 | def foo(x: int) -> int:
|
::: /src/mdtest_snippet.py:4:9
|
2 | foo("hello") # error: [invalid-argument-type]
3 |
4 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
5 | return x * x
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int) -> int:
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:8
|
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -1,46 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(
2 | x: int,
3 | y: int,
4 | z: int,
5 | ) -> int:
6 | return x * y * z
7 |
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:8:8
|
6 | return x * y * z
7 |
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:3:5
|
1 | def foo(
2 | x: int,
3 | y: int,
| ------ info: parameter declared in function definition here
4 | z: int,
5 | ) -> int:
|
```

View File

@@ -1,78 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int) -> int:
2 | return x * y * z
3 |
4 | # error: [invalid-argument-type]
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:5
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["a"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:10
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["b"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:15
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["c"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:25
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -1,41 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | import json
2 |
3 | json.loads(5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:3:12
|
1 | import json
2 |
3 | json.loads(5) # error: [invalid-argument-type]
| ^ Object of type `Literal[5]` cannot be assigned to parameter 1 (`s`) of function `loads`; expected type `str | bytes | bytearray`
|
::: vendored://stdlib/json/__init__.pyi:40:5
|
38 | ) -> None: ...
39 | def loads(
40 | s: str | bytes | bytearray,
| -------------------------- info: parameter declared in function definition here
41 | *,
42 | cls: type[JSONDecoder] | None = None,
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:28
|
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:31
|
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:25
|
1 | def foo(x: int, y: int, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int, /) -> int:
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:8
|
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int, /) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -1,41 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C:
2 | def __call__(self, x: int) -> int:
3 | return 1
4 |
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:6:3
|
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`
|
::: /src/mdtest_snippet.py:2:24
|
1 | class C:
2 | def __call__(self, x: int) -> int:
| ------ info: parameter declared in function definition here
3 | return 1
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(*numbers: int) -> int:
2 | return len(numbers)
3 |
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:14
|
2 | return len(numbers)
3 |
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `*numbers` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(*numbers: int) -> int:
| ------------- info: parameter declared in function definition here
2 | return len(numbers)
|
```

View File

@@ -1,39 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(**numbers: int) -> int:
2 | return len(numbers)
3 |
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:20
|
2 | return len(numbers)
3 |
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `**numbers` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(**numbers: int) -> int:
| -------------- info: parameter declared in function definition here
2 | return len(numbers)
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Right hand side not iterable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = 1 # error: [not-iterable]
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:1:8
|
1 | a, b = 1 # error: [not-iterable]
| ^ Object of type `Literal[1]` is not iterable
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Too few values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = (1,) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ Not enough values to unpack (expected 2, got 1)
|
```

View File

@@ -1,28 +0,0 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Too many values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
| ^^^^ Too many values to unpack (expected 2, got 3)
|
```

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | import does_not_exist # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
--> /src/mdtest_snippet__1.py:1:8
|
1 | import does_not_exist # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`

View File

@@ -16,7 +16,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
2 | does_exist2 = 2
```
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
@@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:28
--> /src/mdtest_snippet__1.py:1:28
|
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from .does_not_exist import add # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:7
--> /src/mdtest_snippet__1.py:1:7
|
1 | from .does_not_exist import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`

View File

@@ -9,7 +9,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
# Python source files
## mdtest_snippet.py
## mdtest_snippet__1.py
```
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
@@ -21,7 +21,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unreso
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:7
--> /src/mdtest_snippet__1.py:1:7
|
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`

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