Compare commits
132 Commits
david/make
...
0.9.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dfa810e9a | ||
|
|
9cd0cdefd3 | ||
|
|
05a4c29344 | ||
|
|
b3c884f4f3 | ||
|
|
6c14225c66 | ||
|
|
0a627ef216 | ||
|
|
a25be4610a | ||
|
|
ce0018c3cb | ||
|
|
48f906e06c | ||
|
|
ebd172e732 | ||
|
|
114abc7cfb | ||
|
|
318f503714 | ||
|
|
d0623888b3 | ||
|
|
23fd4927ae | ||
|
|
cc324abcc2 | ||
|
|
80be0a0115 | ||
|
|
b2e90c3f5c | ||
|
|
d7cbe6b7df | ||
|
|
021640a7a6 | ||
|
|
81bcdcebd3 | ||
|
|
d94a78a134 | ||
|
|
bb44926ca5 | ||
|
|
32c66ec4b7 | ||
|
|
087d92cbf4 | ||
|
|
e7b93f93ef | ||
|
|
c8a06a9be8 | ||
|
|
1977dda079 | ||
|
|
c9ab925275 | ||
|
|
37fbe58b13 | ||
|
|
a3ae76edc0 | ||
|
|
d93ed293eb | ||
|
|
4d92e20e81 | ||
|
|
c4578162d5 | ||
|
|
5d56c2e877 | ||
|
|
c80678a1c0 | ||
|
|
be239b9f25 | ||
|
|
8c899c5409 | ||
|
|
a08f5edf75 | ||
|
|
5efcfd3414 | ||
|
|
79a2c7eaa2 | ||
|
|
eaff95e1ad | ||
|
|
2d9f564ecd | ||
|
|
a6ae86c189 | ||
|
|
08e11e991d | ||
|
|
ec311a7ed0 | ||
|
|
b7de42686f | ||
|
|
ff44500517 | ||
|
|
e924ecbdac | ||
|
|
0d615b8765 | ||
|
|
4431978262 | ||
|
|
ba44e9de13 | ||
|
|
fdf0915283 | ||
|
|
5ca6cc2cc8 | ||
|
|
9bb63495dd | ||
|
|
980faff176 | ||
|
|
0c7c001647 | ||
|
|
09d0b227fb | ||
|
|
091d0af2ab | ||
|
|
3d72138740 | ||
|
|
4a23756024 | ||
|
|
af62f7932b | ||
|
|
0ced8d053c | ||
|
|
a8e171f82c | ||
|
|
cf83584abb | ||
|
|
764aa0e6a1 | ||
|
|
568cf88c6c | ||
|
|
040071bbc5 | ||
|
|
d56d241317 | ||
|
|
7dad0c471d | ||
|
|
fb778ee38d | ||
|
|
671494a620 | ||
|
|
b89d61bd05 | ||
|
|
8c0eac21ab | ||
|
|
c892fee058 | ||
|
|
ea3245b8c4 | ||
|
|
592532738f | ||
|
|
87d011e1bd | ||
|
|
dd6f6233bd | ||
|
|
bf2c9a41cd | ||
|
|
be03cb04c1 | ||
|
|
78806361fd | ||
|
|
b39a4ad01d | ||
|
|
86b01d2d3c | ||
|
|
f88328eedd | ||
|
|
fa76f6cbb2 | ||
|
|
5c007db7e2 | ||
|
|
1be0dc6885 | ||
|
|
a1a536b2c5 | ||
|
|
aac79e453a | ||
|
|
fd7b3c83ad | ||
|
|
d895ee0014 | ||
|
|
4732c58829 | ||
|
|
45bae29a4b | ||
|
|
7059f4249b | ||
|
|
68991d09a8 | ||
|
|
e7a6c19e3a | ||
|
|
42a5f5ef6a | ||
|
|
5bac4f6bd4 | ||
|
|
320a3c68ae | ||
|
|
24e08d17c4 | ||
|
|
141ba253da | ||
|
|
81a57656d8 | ||
|
|
5eaf225fc3 | ||
|
|
bc018bf2e5 | ||
|
|
0fad53d203 | ||
|
|
e6b1c89fb7 | ||
|
|
222588645b | ||
|
|
b7dab13c79 | ||
|
|
81f6561af4 | ||
|
|
c37c078142 | ||
|
|
dd5f9d1df9 | ||
|
|
f05cfe134e | ||
|
|
a3d8b31cdd | ||
|
|
558282649e | ||
|
|
b312b53c2e | ||
|
|
c814745643 | ||
|
|
aa88f2dbe5 | ||
|
|
64effa4aea | ||
|
|
224a36f5f3 | ||
|
|
5347abc766 | ||
|
|
5fab97f1ef | ||
|
|
3aa7ba31b1 | ||
|
|
4dae09ecff | ||
|
|
b9b094869a | ||
|
|
b3c5932fda | ||
|
|
fe3ae587ea | ||
|
|
c2b9fa84f7 | ||
|
|
793264db13 | ||
|
|
4d63c16c19 | ||
|
|
d2e034adcd | ||
|
|
f62e5406f2 | ||
|
|
1be4394155 |
31
.github/ISSUE_TEMPLATE/1_bug_report.yaml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/1_bug_report.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Bug report
|
||||
description: Report an error or unexpected behavior
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to report an issue! We're glad to have you involved with Ruff.
|
||||
|
||||
**Before reporting, please make sure to search through [existing issues](https://github.com/astral-sh/ruff/issues?q=is:issue+is:open+label:bug) (including [closed](https://github.com/astral-sh/ruff/issues?q=is:issue%20state:closed%20label:bug)).**
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: |
|
||||
A clear and concise description of the bug, including a minimal reproducible example.
|
||||
|
||||
Be sure to include the command you invoked (e.g., `ruff check /path/to/file.py --fix`), ideally including the `--isolated` flag and
|
||||
the current Ruff settings (e.g., relevant sections from your `pyproject.toml`).
|
||||
|
||||
If possible, try to include the [playground](https://play.ruff.rs) link that reproduces this issue.
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of ruff are you using? (see `ruff version`)
|
||||
placeholder: e.g., ruff 0.9.3 (90589372d 2025-01-23)
|
||||
validations:
|
||||
required: false
|
||||
10
.github/ISSUE_TEMPLATE/2_rule_request.yaml
vendored
Normal file
10
.github/ISSUE_TEMPLATE/2_rule_request.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: Rule request
|
||||
description: Anything related to lint rules (proposing new rules, changes to existing rules, auto-fixes, etc.)
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: |
|
||||
A clear and concise description of the relevant request. If applicable, please describe the current behavior as well.
|
||||
validations:
|
||||
required: true
|
||||
18
.github/ISSUE_TEMPLATE/3_question.yaml
vendored
Normal file
18
.github/ISSUE_TEMPLATE/3_question.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Question
|
||||
description: Ask a question about Ruff
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Question
|
||||
description: Describe your question in detail.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of ruff are you using? (see `ruff version`)
|
||||
placeholder: e.g., ruff 0.9.3 (90589372d 2025-01-23)
|
||||
validations:
|
||||
required: false
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,2 +1,8 @@
|
||||
# This file cannot use the extension `.yaml`.
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://docs.astral.sh/ruff
|
||||
about: Please consult the documentation before creating an issue.
|
||||
- name: Community
|
||||
url: https://discord.com/invite/astral-sh
|
||||
about: Join our Discord community to ask questions and collaborate.
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/issue.yaml
vendored
22
.github/ISSUE_TEMPLATE/issue.yaml
vendored
@@ -1,22 +0,0 @@
|
||||
name: New issue
|
||||
description: A generic issue
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to report an issue! We're glad to have you involved with Ruff.
|
||||
|
||||
If you're filing a bug report, please consider including the following information:
|
||||
|
||||
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
|
||||
e.g. "RUF001", "unused variable", "Jupyter notebook"
|
||||
* A minimal code snippet that reproduces the bug.
|
||||
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
|
||||
* The current Ruff settings (any relevant sections from your `pyproject.toml`).
|
||||
* The current Ruff version (`ruff --version`).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A description of the issue
|
||||
15
.github/renovate.json5
vendored
15
.github/renovate.json5
vendored
@@ -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
|
||||
@@ -101,14 +95,7 @@
|
||||
matchManagers: ["cargo"],
|
||||
matchPackageNames: ["strum"],
|
||||
description: "Weekly update of strum dependencies",
|
||||
},
|
||||
{
|
||||
groupName: "ESLint",
|
||||
matchManagers: ["npm"],
|
||||
matchPackageNames: ["eslint"],
|
||||
allowedVersions: "<9",
|
||||
description: "Constraint ESLint to version 8 until TypeScript-eslint supports ESLint 9", // https://github.com/typescript-eslint/typescript-eslint/issues/8211
|
||||
},
|
||||
}
|
||||
],
|
||||
vulnerabilityAlerts: {
|
||||
commitMessageSuffix: "",
|
||||
|
||||
1
.github/workflows/daily_property_tests.yaml
vendored
1
.github/workflows/daily_property_tests.yaml
vendored
@@ -47,6 +47,7 @@ jobs:
|
||||
run: |
|
||||
export QUICKCHECK_TESTS=100000
|
||||
for _ in {1..5}; do
|
||||
cargo test --locked --release --package red_knot_python_semantic -- --ignored list::property_tests
|
||||
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
|
||||
done
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.29.7
|
||||
rev: v1.30.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -74,7 +74,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.6
|
||||
rev: v0.9.9
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -84,7 +84,7 @@ repos:
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.5.1
|
||||
rev: v3.5.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
@@ -92,12 +92,12 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.3.1
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.31.1
|
||||
rev: 0.31.2
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,5 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.10
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`ruff`\] Add new rule `RUF059`: Unused unpacked assignment ([#16449](https://github.com/astral-sh/ruff/pull/16449))
|
||||
- \[`syntax-errors`\] Detect assignment expressions before Python 3.8 ([#16383](https://github.com/astral-sh/ruff/pull/16383))
|
||||
- \[`syntax-errors`\] Named expressions in decorators before Python 3.9 ([#16386](https://github.com/astral-sh/ruff/pull/16386))
|
||||
- \[`syntax-errors`\] Parenthesized keyword argument names after Python 3.8 ([#16482](https://github.com/astral-sh/ruff/pull/16482))
|
||||
- \[`syntax-errors`\] Positional-only parameters before Python 3.8 ([#16481](https://github.com/astral-sh/ruff/pull/16481))
|
||||
- \[`syntax-errors`\] Tuple unpacking in `return` and `yield` before Python 3.8 ([#16485](https://github.com/astral-sh/ruff/pull/16485))
|
||||
- \[`syntax-errors`\] Type parameter defaults before Python 3.13 ([#16447](https://github.com/astral-sh/ruff/pull/16447))
|
||||
- \[`syntax-errors`\] Type parameter lists before Python 3.12 ([#16479](https://github.com/astral-sh/ruff/pull/16479))
|
||||
- \[`syntax-errors`\] `except*` before Python 3.11 ([#16446](https://github.com/astral-sh/ruff/pull/16446))
|
||||
- \[`syntax-errors`\] `type` statements before Python 3.12 ([#16478](https://github.com/astral-sh/ruff/pull/16478))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Escape template filenames in glob patterns in configuration ([#16407](https://github.com/astral-sh/ruff/pull/16407))
|
||||
- \[`flake8-simplify`\] Exempt unittest context methods for `SIM115` rule ([#16439](https://github.com/astral-sh/ruff/pull/16439))
|
||||
- Formatter: Fix syntax error location in notebooks ([#16499](https://github.com/astral-sh/ruff/pull/16499))
|
||||
- \[`pyupgrade`\] Do not offer fix when at least one target is `global`/`nonlocal` (`UP028`) ([#16451](https://github.com/astral-sh/ruff/pull/16451))
|
||||
- \[`flake8-builtins`\] Ignore variables matching module attribute names (`A001`) ([#16454](https://github.com/astral-sh/ruff/pull/16454))
|
||||
- \[`pylint`\] Convert `code` keyword argument to a positional argument in fix for (`PLR1722`) ([#16424](https://github.com/astral-sh/ruff/pull/16424))
|
||||
|
||||
### CLI
|
||||
|
||||
- Move rule code from `description` to `check_name` in GitLab output serializer ([#16437](https://github.com/astral-sh/ruff/pull/16437))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`pydocstyle`\] Clarify that `D417` only checks docstrings with an arguments section ([#16494](https://github.com/astral-sh/ruff/pull/16494))
|
||||
|
||||
## 0.9.9
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix caching of unsupported-syntax errors ([#16425](https://github.com/astral-sh/ruff/pull/16425))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Only show unsupported-syntax errors in editors when preview mode is enabled ([#16429](https://github.com/astral-sh/ruff/pull/16429))
|
||||
|
||||
## 0.9.8
|
||||
|
||||
### Preview features
|
||||
|
||||
- Start detecting version-related syntax errors in the parser ([#16090](https://github.com/astral-sh/ruff/pull/16090))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pylint`\] Mark fix unsafe (`PLW1507`) ([#16343](https://github.com/astral-sh/ruff/pull/16343))
|
||||
- \[`pylint`\] Catch `case np.nan`/`case math.nan` in `match` statements (`PLW0177`) ([#16378](https://github.com/astral-sh/ruff/pull/16378))
|
||||
- \[`ruff`\] Add more Pydantic models variants to the list of default copy semantics (`RUF012`) ([#16291](https://github.com/astral-sh/ruff/pull/16291))
|
||||
|
||||
### Server
|
||||
|
||||
- Avoid indexing the project if `configurationPreference` is `editorOnly` ([#16381](https://github.com/astral-sh/ruff/pull/16381))
|
||||
- Avoid unnecessary info at non-trace server log level ([#16389](https://github.com/astral-sh/ruff/pull/16389))
|
||||
- Expand `ruff.configuration` to allow inline config ([#16296](https://github.com/astral-sh/ruff/pull/16296))
|
||||
- Notify users for invalid client settings ([#16361](https://github.com/astral-sh/ruff/pull/16361))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Add `per-file-target-version` option ([#16257](https://github.com/astral-sh/ruff/pull/16257))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`refurb`\] Do not consider docstring(s) (`FURB156`) ([#16391](https://github.com/astral-sh/ruff/pull/16391))
|
||||
- \[`flake8-self`\] Ignore attribute accesses on instance-like variables (`SLF001`) ([#16149](https://github.com/astral-sh/ruff/pull/16149))
|
||||
- \[`pylint`\] Fix false positives, add missing methods, and support positional-only parameters (`PLE0302`) ([#16263](https://github.com/astral-sh/ruff/pull/16263))
|
||||
- \[`flake8-pyi`\] Mark `PYI030` fix unsafe when comments are deleted ([#16322](https://github.com/astral-sh/ruff/pull/16322))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix example for `S611` ([#16316](https://github.com/astral-sh/ruff/pull/16316))
|
||||
- Normalize inconsistent markdown headings in docstrings ([#16364](https://github.com/astral-sh/ruff/pull/16364))
|
||||
- Document MSRV policy ([#16384](https://github.com/astral-sh/ruff/pull/16384))
|
||||
|
||||
## 0.9.7
|
||||
|
||||
### Preview features
|
||||
@@ -13,16 +91,7 @@
|
||||
|
||||
### 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
|
||||
@@ -43,7 +112,16 @@
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-comprehensions`\] Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110))
|
||||
- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141))
|
||||
- \[`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))
|
||||
- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237))
|
||||
- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122))
|
||||
|
||||
### Documentation
|
||||
|
||||
|
||||
297
Cargo.lock
generated
297
Cargo.lock
generated
@@ -8,18 +8,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -136,21 +124,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7992085ec035cfe96992dd31bfd495a2ebd31969bb95f624471cb6c0b349e571"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
@@ -212,9 +188,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -227,9 +203,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.8"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42"
|
||||
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
@@ -321,14 +300,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.39"
|
||||
version = "0.4.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -360,9 +339,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.29"
|
||||
version = "4.5.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
|
||||
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -370,9 +349,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.29"
|
||||
version = "4.5.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
|
||||
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -444,9 +423,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "2.8.0"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25d2f5a6570db487f5258e0bded6352fa2034c2aeb46bb5cc3ff060a0fcfba2f"
|
||||
checksum = "de4b67ff8985f3993f06167d71cf4aec178b0a1580f91a987170c59d60021103"
|
||||
dependencies = [
|
||||
"colored 2.2.0",
|
||||
"libc",
|
||||
@@ -457,9 +436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat"
|
||||
version = "2.8.0"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f53a55558dedec742b14aae3c5fec389361b8b5ca28c1aadf09dd91faf710074"
|
||||
checksum = "68403d768ed1def18a87e2306676781314448393ecf0d3057c4527cabf524a3d"
|
||||
dependencies = [
|
||||
"codspeed",
|
||||
"colored 2.2.0",
|
||||
@@ -479,7 +458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -488,7 +467,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -905,7 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1013,6 +992,19 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -1065,9 +1057,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.15"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
|
||||
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
@@ -1082,7 +1074,7 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -1102,10 +1094,6 @@ name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -1113,17 +1101,18 @@ version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1179,7 +1168,7 @@ dependencies = [
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1408,7 +1397,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
@@ -1424,9 +1413,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.42.1"
|
||||
version = "1.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86"
|
||||
checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084"
|
||||
dependencies = [
|
||||
"console",
|
||||
"globset",
|
||||
@@ -1482,7 +1471,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
|
||||
dependencies = [
|
||||
"hermit-abi 0.4.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1587,9 +1576,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.169"
|
||||
version = "0.2.170"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1632,7 +1621,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
@@ -1679,9 +1668,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.25"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp-server"
|
||||
@@ -1802,7 +1804,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -1830,7 +1832,7 @@ version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
@@ -2401,6 +2403,7 @@ name = "red_knot"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored 3.0.0",
|
||||
@@ -2425,6 +2428,7 @@ dependencies = [
|
||||
"tracing-flame",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
"wild",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2459,7 +2463,7 @@ name = "red_knot_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"camino",
|
||||
"compact_str",
|
||||
"countme",
|
||||
@@ -2491,6 +2495,8 @@ dependencies = [
|
||||
"serde",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.11",
|
||||
@@ -2535,6 +2541,7 @@ dependencies = [
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
@@ -2543,6 +2550,8 @@ dependencies = [
|
||||
"salsa",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"thiserror 2.0.11",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -2579,7 +2588,7 @@ version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2650,13 +2659,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.7"
|
||||
version = "0.9.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"assert_fs",
|
||||
"bincode",
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"cachedir",
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -2686,6 +2695,7 @@ dependencies = [
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
"ruff_server",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
@@ -2884,11 +2894,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.7"
|
||||
version = "0.9.10"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored 3.0.0",
|
||||
@@ -2978,7 +2988,7 @@ name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"compact_str",
|
||||
"is-macro",
|
||||
"itertools 0.14.0",
|
||||
@@ -3062,7 +3072,7 @@ dependencies = [
|
||||
name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"itertools 0.14.0",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
@@ -3073,7 +3083,7 @@ name = "ruff_python_parser"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"bstr",
|
||||
"compact_str",
|
||||
"insta",
|
||||
@@ -3084,6 +3094,8 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"static_assertions",
|
||||
"unicode-ident",
|
||||
"unicode-normalization",
|
||||
@@ -3105,7 +3117,7 @@ dependencies = [
|
||||
name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
"ruff_index",
|
||||
@@ -3124,7 +3136,7 @@ dependencies = [
|
||||
name = "ruff_python_stdlib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -3178,6 +3190,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.11",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -3203,7 +3216,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.9.7"
|
||||
version = "0.9.10"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3293,11 +3306,11 @@ version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3315,14 +3328,13 @@ 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=99be5d9917c3dd88e19735a82ef6bf39ba84bd7e#99be5d9917c3dd88e19735a82ef6bf39ba84bd7e"
|
||||
dependencies = [
|
||||
"append-only-vec",
|
||||
"arc-swap",
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
"crossbeam",
|
||||
"crossbeam-queue",
|
||||
"dashmap 6.1.0",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.2",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"parking_lot",
|
||||
@@ -3337,12 +3349,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=99be5d9917c3dd88e19735a82ef6bf39ba84bd7e#99be5d9917c3dd88e19735a82ef6bf39ba84bd7e"
|
||||
|
||||
[[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=99be5d9917c3dd88e19735a82ef6bf39ba84bd7e#99be5d9917c3dd88e19735a82ef6bf39ba84bd7e"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3362,9 +3374,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
@@ -3374,9 +3386,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3384,6 +3396,12 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -3398,9 +3416,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3418,9 +3436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3440,9 +3458,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.138"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3668,16 +3686,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.17.0"
|
||||
version = "3.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33"
|
||||
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3971,6 +3989,7 @@ version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"matchers",
|
||||
"nu-ansi-term 0.46.0",
|
||||
"once_cell",
|
||||
@@ -4068,9 +4087,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.16"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -4442,7 +4461,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4451,6 +4470,16 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -4460,6 +4489,66 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -4629,7 +4718,7 @@ version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.83"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -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 = "99be5d9917c3dd88e19735a82ef6bf39ba84bd7e" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -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.10/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.10/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.7
|
||||
rev: v0.9.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -23,6 +23,10 @@ extend-ignore-re = [
|
||||
# Line ignore with trailing "spellchecker:disable-line"
|
||||
"(?Rm)^.*#\\s*spellchecker:disable-line$",
|
||||
"LICENSEs",
|
||||
# Various third party dependencies uses `typ` as struct field names (e.g., lsp_types::LogMessageParams)
|
||||
"typ",
|
||||
# TODO: Remove this once the `TYP` redirects are removed from `rule_redirects.rs`
|
||||
"TYP",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
|
||||
@@ -19,6 +19,7 @@ ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
colored = { workspace = true }
|
||||
@@ -31,6 +32,7 @@ tracing = { workspace = true, features = ["release_max_level_debug"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||
tracing-flame = { workspace = true }
|
||||
tracing-tree = { workspace = true }
|
||||
wild = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
|
||||
@@ -32,6 +32,13 @@ pub(crate) enum Command {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub(crate) struct CheckCommand {
|
||||
/// List of files or directories to check.
|
||||
#[clap(
|
||||
help = "List of files or directories to check [default: the project root]",
|
||||
value_name = "PATH"
|
||||
)]
|
||||
pub paths: Vec<SystemPathBuf>,
|
||||
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
@@ -41,12 +48,14 @@ pub(crate) struct CheckCommand {
|
||||
#[arg(long, value_name = "PROJECT")]
|
||||
pub(crate) project: Option<SystemPathBuf>,
|
||||
|
||||
/// Path to the virtual environment the project uses.
|
||||
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
|
||||
///
|
||||
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
|
||||
/// to resolve type information for the project's third-party dependencies.
|
||||
/// Red Knot will search in the path's `site-packages` directories for type information and
|
||||
/// third-party imports.
|
||||
///
|
||||
/// This option is commonly used to specify the path to a virtual environment.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub(crate) venv_path: Option<SystemPathBuf>,
|
||||
pub(crate) python: Option<SystemPathBuf>,
|
||||
|
||||
/// Custom directory to use for stdlib typeshed stubs.
|
||||
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
|
||||
@@ -74,7 +83,7 @@ pub(crate) struct CheckCommand {
|
||||
#[arg(long)]
|
||||
pub(crate) exit_zero: bool,
|
||||
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
/// Watch files for changes and recheck files related to the changed files.
|
||||
#[arg(long, short = 'W')]
|
||||
pub(crate) watch: bool,
|
||||
}
|
||||
@@ -97,7 +106,7 @@ impl CheckCommand {
|
||||
python_version: self
|
||||
.python_version
|
||||
.map(|version| RangedValue::cli(version.into())),
|
||||
venv_path: self.venv_path.map(RelativePathBuf::cli),
|
||||
python: self.python.map(RelativePathBuf::cli),
|
||||
typeshed: self.typeshed.map(RelativePathBuf::cli),
|
||||
extra_paths: self.extra_search_path.map(|extra_search_paths| {
|
||||
extra_search_paths
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::io::{self, BufWriter, Write};
|
||||
use std::io::{self, stdout, BufWriter, Write};
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -15,8 +15,8 @@ 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::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait, Severity};
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
mod args;
|
||||
@@ -39,6 +39,15 @@ pub fn main() -> ExitStatus {
|
||||
// the configuration it is help to chain errors ("resolving configuration failed" ->
|
||||
// "failed to read file: subdir/pyproject.toml")
|
||||
for cause in error.chain() {
|
||||
// Exit "gracefully" on broken pipe errors.
|
||||
//
|
||||
// See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14
|
||||
if let Some(ioerr) = cause.downcast_ref::<io::Error>() {
|
||||
if ioerr.kind() == io::ErrorKind::BrokenPipe {
|
||||
return ExitStatus::Success;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(stderr, " {} {cause}", "Cause:".bold()).ok();
|
||||
}
|
||||
|
||||
@@ -47,7 +56,10 @@ pub fn main() -> ExitStatus {
|
||||
}
|
||||
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let args = Args::parse_from(std::env::args());
|
||||
let args = wild::args_os();
|
||||
let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
|
||||
.context("Failed to read CLI arguments from file")?;
|
||||
let args = Args::parse_from(args);
|
||||
|
||||
match args.command {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
@@ -69,7 +81,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cli_base_path = {
|
||||
let cwd = {
|
||||
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
|
||||
SystemPathBuf::from_path_buf(cwd)
|
||||
.map_err(|path| {
|
||||
@@ -80,30 +92,42 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
})?
|
||||
};
|
||||
|
||||
let cwd = args
|
||||
let project_path = args
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|cwd| {
|
||||
if cwd.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(cwd, &cli_base_path))
|
||||
.map(|project| {
|
||||
if project.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(project, &cwd))
|
||||
} else {
|
||||
Err(anyhow!("Provided project path `{cwd}` is not a directory"))
|
||||
Err(anyhow!(
|
||||
"Provided project path `{project}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| cli_base_path.clone());
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
|
||||
let check_paths: Vec<_> = args
|
||||
.paths
|
||||
.iter()
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut project_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
|
||||
if !check_paths.is_empty() {
|
||||
db.project().set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
@@ -119,7 +143,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
let exit_status = if watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db)
|
||||
main_loop.run(&mut db)?
|
||||
};
|
||||
|
||||
tracing::trace!("Counts for entire CLI run:\n{}", countme::get_all());
|
||||
@@ -179,7 +203,7 @@ impl MainLoop {
|
||||
)
|
||||
}
|
||||
|
||||
fn watch(mut self, db: &mut ProjectDatabase) -> anyhow::Result<ExitStatus> {
|
||||
fn watch(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
tracing::debug!("Starting watch mode");
|
||||
let sender = self.sender.clone();
|
||||
let watcher = watch::directory_watcher(move |event| {
|
||||
@@ -188,12 +212,12 @@ impl MainLoop {
|
||||
|
||||
self.watcher = Some(ProjectWatcher::new(watcher, db));
|
||||
|
||||
self.run(db);
|
||||
self.run(db)?;
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn run(mut self, db: &mut ProjectDatabase) -> ExitStatus {
|
||||
fn run(mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
|
||||
let result = self.main_loop(db);
|
||||
@@ -203,7 +227,7 @@ impl MainLoop {
|
||||
result
|
||||
}
|
||||
|
||||
fn main_loop(&mut self, db: &mut ProjectDatabase) -> ExitStatus {
|
||||
fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result<ExitStatus> {
|
||||
// Schedule the first check.
|
||||
tracing::debug!("Starting main loop");
|
||||
|
||||
@@ -241,14 +265,43 @@ impl MainLoop {
|
||||
Severity::Error
|
||||
};
|
||||
|
||||
let failed = result
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
|
||||
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
for diagnostic in result {
|
||||
println!("{}", diagnostic.display(db, &display_config));
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
||||
let mut stdout = stdout().lock();
|
||||
|
||||
if result.is_empty() {
|
||||
writeln!(stdout, "All checks passed!")?;
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
} else {
|
||||
let mut failed = false;
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
writeln!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
|
||||
failed |= diagnostic.severity() >= min_error_severity;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
stdout,
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(if failed {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -256,14 +309,6 @@ impl MainLoop {
|
||||
);
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return if failed {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
};
|
||||
}
|
||||
|
||||
tracing::trace!("Counts after last check:\n{}", countme::get_all());
|
||||
}
|
||||
|
||||
@@ -281,14 +326,14 @@ impl MainLoop {
|
||||
// TODO: Don't use Salsa internal APIs
|
||||
// [Zulip-Thread](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/Expose.20an.20API.20to.20cancel.20other.20queries)
|
||||
let _ = db.zalsa_mut();
|
||||
return ExitStatus::Success;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Waiting for next main loop message.");
|
||||
}
|
||||
|
||||
ExitStatus::Success
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +353,8 @@ impl MainLoopCancellationToken {
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
result: Vec<Box<dyn Diagnostic>>,
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Box<dyn OldDiagnosticTrait>>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
|
||||
@@ -28,7 +28,7 @@ fn config_override() -> anyhow::Result<()> {
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -40,16 +40,18 @@ fn config_override() -> anyhow::Result<()> {
|
||||
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
@@ -98,7 +100,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.root().join("child")), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -111,16 +113,18 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
4 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
@@ -168,11 +172,12 @@ 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"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
@@ -195,7 +200,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -217,9 +222,10 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
case.write_file(
|
||||
"pyproject.toml",
|
||||
@@ -230,7 +236,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -243,9 +249,10 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
4 | for a in range(0, y):
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -269,7 +276,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -302,9 +309,10 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
@@ -315,7 +323,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
.arg("division-by-zero")
|
||||
.arg("--warn")
|
||||
.arg("unresolved-import"),
|
||||
@r###"
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -339,9 +347,10 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
6 | for a in range(0, y):
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -365,7 +374,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -387,9 +396,10 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
@@ -401,7 +411,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
// Override the error severity with warning
|
||||
.arg("--ignore")
|
||||
.arg("possibly-unresolved-reference"),
|
||||
@r###"
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -414,9 +424,10 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
4 | for a in range(0, y):
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -436,7 +447,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
("test.py", "print(10)"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -448,9 +459,10 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
| --------------- Unknown lint rule `division-by-zer`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -460,15 +472,16 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", "print(10)")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: unknown-rule: Unknown lint rule `division-by-zer`
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -477,7 +490,7 @@ fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||
fn exit_code_only_warnings() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -488,9 +501,10 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
|
||||
| - Name `x` used when not defined
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -505,7 +519,7 @@ fn exit_code_only_info() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -517,9 +531,10 @@ fn exit_code_only_info() -> anyhow::Result<()> {
|
||||
| -------------- info: Revealed type is `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -534,7 +549,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -546,9 +561,10 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
| -------------- info: Revealed type is `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -557,7 +573,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -568,9 +584,10 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
| - Name `x` used when not defined
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -588,7 +605,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -599,9 +616,10 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
|
||||
| - Name `x` used when not defined
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -616,7 +634,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -636,9 +654,10 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -653,7 +672,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
|
||||
"###,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -673,9 +692,10 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
|
||||
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -690,7 +710,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r###"
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -710,9 +730,10 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -749,7 +770,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
|
||||
@r###"
|
||||
@r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -771,9 +792,10 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
"
|
||||
);
|
||||
|
||||
// The user-level configuration promotes `possibly-unresolved-reference` to an error.
|
||||
@@ -790,7 +812,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
|
||||
@r###"
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -812,9 +834,134 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
| ^ Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_specific_paths() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"project/main.py",
|
||||
r#"
|
||||
y = 4 / 0 # error: division-by-zero
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/tests/test_main.py",
|
||||
r#"
|
||||
import does_not_exist # error: unresolved-import
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/other.py",
|
||||
r#"
|
||||
from main2 import z # error: unresolved-import
|
||||
|
||||
print(z)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command(),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
|
|
||||
|
||||
error: lint:division-by-zero
|
||||
--> <temp_dir>/project/main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0 # error: division-by-zero
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
|
|
||||
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
| ^^^^^ Cannot resolve import `main2`
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
|
||||
Found 3 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
// Now check only the `tests` and `other.py` files.
|
||||
// We should no longer see any diagnostics related to `main.py`.
|
||||
assert_cmd_snapshot!(
|
||||
case.command().arg("project/tests").arg("project/other.py"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/project/tests/test_main.py:2:8
|
||||
|
|
||||
2 | import does_not_exist # error: unresolved-import
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
|
|
||||
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/project/other.py:2:6
|
||||
|
|
||||
2 | from main2 import z # error: unresolved-import
|
||||
| ^^^^^ Cannot resolve import `main2`
|
||||
3 |
|
||||
4 | print(z)
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_non_existing_path() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([])?;
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(
|
||||
®ex::escape("The system cannot find the path specified. (os error 3)"),
|
||||
"No such file or directory (os error 2)",
|
||||
);
|
||||
let _s = settings.bind_to_scope();
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case.command().arg("project/main.py").arg("project/tests"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: io: `<temp_dir>/project/main.py`: No such file or directory (os error 2)
|
||||
|
||||
error: io: `<temp_dir>/project/tests`: No such file or directory (os error 2)
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
WARN No python files found under the given path(s)
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::io::Write;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -193,11 +194,29 @@ impl TestCase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_project_files(&self) -> Vec<File> {
|
||||
let files = self.db().project().files(self.db());
|
||||
let mut collected: Vec<_> = files.into_iter().collect();
|
||||
collected.sort_unstable_by_key(|file| file.path(self.db()).as_system_path().unwrap());
|
||||
collected
|
||||
#[track_caller]
|
||||
fn assert_indexed_project_files(&self, expected: impl IntoIterator<Item = File>) {
|
||||
let mut expected: HashSet<_> = expected.into_iter().collect();
|
||||
|
||||
let actual = self.db().project().files(self.db());
|
||||
for file in &actual {
|
||||
assert!(
|
||||
expected.remove(&file),
|
||||
"Indexed project files contains '{}' which was not expected.",
|
||||
file.path(self.db())
|
||||
);
|
||||
}
|
||||
|
||||
if !expected.is_empty() {
|
||||
let paths: Vec<_> = expected
|
||||
.iter()
|
||||
.map(|file| file.path(self.db()).as_str())
|
||||
.collect();
|
||||
panic!(
|
||||
"Indexed project files are missing the following files: {:?}",
|
||||
paths.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn system_file(&self, path: impl AsRef<SystemPath>) -> Result<File, FileError> {
|
||||
@@ -222,13 +241,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
trait SetupFiles {
|
||||
fn setup(self, context: &SetupContext) -> anyhow::Result<()>;
|
||||
trait Setup {
|
||||
fn setup(self, context: &mut SetupContext) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
struct SetupContext<'a> {
|
||||
system: &'a OsSystem,
|
||||
root_path: &'a SystemPath,
|
||||
options: Option<Options>,
|
||||
included_paths: Option<Vec<SystemPathBuf>>,
|
||||
}
|
||||
|
||||
impl<'a> SetupContext<'a> {
|
||||
@@ -251,55 +272,77 @@ impl<'a> SetupContext<'a> {
|
||||
fn join_root_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
|
||||
self.root_path().join(relative)
|
||||
}
|
||||
|
||||
fn write_project_file(
|
||||
&self,
|
||||
relative_path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let relative_path = relative_path.as_ref();
|
||||
let absolute_path = self.join_project_path(relative_path);
|
||||
Self::write_file_impl(absolute_path, content)
|
||||
}
|
||||
|
||||
fn write_file(
|
||||
&self,
|
||||
relative_path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let relative_path = relative_path.as_ref();
|
||||
let absolute_path = self.join_root_path(relative_path);
|
||||
Self::write_file_impl(absolute_path, content)
|
||||
}
|
||||
|
||||
fn write_file_impl(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create parent directory for file `{path}`"))?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(path.as_std_path())
|
||||
.with_context(|| format!("Failed to open file `{path}`"))?;
|
||||
file.write_all(content.as_bytes())
|
||||
.with_context(|| format!("Failed to write to file `{path}`"))?;
|
||||
file.sync_data()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_options(&mut self, options: Options) {
|
||||
self.options = Some(options);
|
||||
}
|
||||
|
||||
fn set_included_paths(&mut self, paths: Vec<SystemPathBuf>) {
|
||||
self.included_paths = Some(paths);
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
|
||||
impl<const N: usize, P> Setup for [(P, &'static str); N]
|
||||
where
|
||||
P: AsRef<SystemPath>,
|
||||
{
|
||||
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
|
||||
fn setup(self, context: &mut SetupContext) -> anyhow::Result<()> {
|
||||
for (relative_path, content) in self {
|
||||
let relative_path = relative_path.as_ref();
|
||||
let absolute_path = context.join_project_path(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}`")
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(absolute_path.as_std_path())
|
||||
.with_context(|| format!("Failed to open file `{relative_path}`"))?;
|
||||
file.write_all(content.as_bytes())
|
||||
.with_context(|| format!("Failed to write to file `{relative_path}`"))?;
|
||||
file.sync_data()?;
|
||||
context.write_project_file(relative_path, content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> SetupFiles for F
|
||||
impl<F> Setup for F
|
||||
where
|
||||
F: FnOnce(&SetupContext) -> anyhow::Result<()>,
|
||||
F: FnOnce(&mut SetupContext) -> anyhow::Result<()>,
|
||||
{
|
||||
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
|
||||
fn setup(self, context: &mut SetupContext) -> anyhow::Result<()> {
|
||||
self(context)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
{
|
||||
setup_with_options(setup_files, |_context| None)
|
||||
}
|
||||
|
||||
fn setup_with_options<F>(
|
||||
setup_files: F,
|
||||
create_options: impl FnOnce(&SetupContext) -> Option<Options>,
|
||||
) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
F: Setup,
|
||||
{
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
||||
@@ -325,16 +368,18 @@ where
|
||||
.with_context(|| format!("Failed to create project directory `{project_path}`"))?;
|
||||
|
||||
let system = OsSystem::new(&project_path);
|
||||
let setup_context = SetupContext {
|
||||
let mut setup_context = SetupContext {
|
||||
system: &system,
|
||||
root_path: &root_path,
|
||||
options: None,
|
||||
included_paths: None,
|
||||
};
|
||||
|
||||
setup_files
|
||||
.setup(&setup_context)
|
||||
.setup(&mut setup_context)
|
||||
.context("Failed to setup test files")?;
|
||||
|
||||
if let Some(options) = create_options(&setup_context) {
|
||||
if let Some(options) = setup_context.options {
|
||||
std::fs::write(
|
||||
project_path.join("pyproject.toml").as_std_path(),
|
||||
toml::to_string(&PyProject {
|
||||
@@ -348,6 +393,8 @@ where
|
||||
.context("Failed to write configuration")?;
|
||||
}
|
||||
|
||||
let included_paths = setup_context.included_paths;
|
||||
|
||||
let mut project = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project.apply_configuration_files(&system)?;
|
||||
|
||||
@@ -363,7 +410,11 @@ where
|
||||
.with_context(|| format!("Failed to create search path `{path}`"))?;
|
||||
}
|
||||
|
||||
let db = ProjectDatabase::new(project, system)?;
|
||||
let mut db = ProjectDatabase::new(project, system)?;
|
||||
|
||||
if let Some(included_paths) = included_paths {
|
||||
db.project().set_included_paths(&mut db, included_paths);
|
||||
}
|
||||
|
||||
let (sender, receiver) = crossbeam::channel::unbounded();
|
||||
let watcher = directory_watcher(move |events| sender.send(events).unwrap())
|
||||
@@ -425,7 +476,7 @@ fn new_file() -> anyhow::Result<()> {
|
||||
let foo_path = case.project_path("foo.py");
|
||||
|
||||
assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
|
||||
assert_eq!(&case.collect_project_files(), &[bar_file]);
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
@@ -435,7 +486,7 @@ fn new_file() -> anyhow::Result<()> {
|
||||
|
||||
let foo = case.system_file(&foo_path).expect("foo.py to exist.");
|
||||
|
||||
assert_eq!(&case.collect_project_files(), &[bar_file, foo]);
|
||||
case.assert_indexed_project_files([bar_file, foo]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -448,7 +499,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
let foo_path = case.project_path("foo.py");
|
||||
|
||||
assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
|
||||
assert_eq!(&case.collect_project_files(), &[bar_file]);
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
|
||||
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
@@ -457,7 +508,132 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
assert_eq!(&case.collect_project_files(), &[bar_file]);
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_non_project_file() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
context.write_project_file("bar.py", "")?;
|
||||
context.set_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
context.join_root_path("site_packages"),
|
||||
)]),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let bar_path = case.project_path("bar.py");
|
||||
let bar_file = case.system_file(&bar_path).unwrap();
|
||||
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
|
||||
// Add a file to site packages
|
||||
let black_path = case.root_path().join("site_packages/black.py");
|
||||
std::fs::write(black_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("black.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(case.system_file(&black_path).is_ok());
|
||||
|
||||
// The file should not have been added to the project files
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_files_with_explicit_included_paths() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
context.write_project_file("src/main.py", "")?;
|
||||
context.write_project_file("src/sub/__init__.py", "")?;
|
||||
context.write_project_file("src/test.py", "")?;
|
||||
context.set_included_paths(vec![
|
||||
context.join_project_path("src/main.py"),
|
||||
context.join_project_path("src/sub"),
|
||||
]);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let main_path = case.project_path("src/main.py");
|
||||
let main_file = case.system_file(&main_path).unwrap();
|
||||
|
||||
let sub_init_path = case.project_path("src/sub/__init__.py");
|
||||
let sub_init = case.system_file(&sub_init_path).unwrap();
|
||||
|
||||
case.assert_indexed_project_files([main_file, sub_init]);
|
||||
|
||||
// Write a new file to `sub` which is an included path
|
||||
let sub_a_path = case.project_path("src/sub/a.py");
|
||||
std::fs::write(sub_a_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
// and write a second file in the root directory -- this should not be included
|
||||
let test2_path = case.project_path("src/test2.py");
|
||||
std::fs::write(test2_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("test2.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
let sub_a_file = case.system_file(&sub_a_path).expect("sub/a.py to exist");
|
||||
|
||||
case.assert_indexed_project_files([main_file, sub_init, sub_a_file]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_file_in_included_out_of_project_directory() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
context.write_project_file("src/main.py", "")?;
|
||||
context.write_project_file("script.py", "")?;
|
||||
context.write_file("outside_project/a.py", "")?;
|
||||
|
||||
context.set_included_paths(vec![
|
||||
context.join_root_path("outside_project"),
|
||||
context.join_project_path("src"),
|
||||
]);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let main_path = case.project_path("src/main.py");
|
||||
let main_file = case.system_file(&main_path).unwrap();
|
||||
|
||||
let outside_a_path = case.root_path().join("outside_project/a.py");
|
||||
let outside_a = case.system_file(&outside_a_path).unwrap();
|
||||
|
||||
case.assert_indexed_project_files([outside_a, main_file]);
|
||||
|
||||
// Write a new file to `src` which should be watched
|
||||
let src_a = case.project_path("src/a.py");
|
||||
std::fs::write(src_a.as_std_path(), "print('Hello')")?;
|
||||
|
||||
// and write a second file to `outside_project` which should be watched too
|
||||
let outside_b_path = case.root_path().join("outside_project/b.py");
|
||||
std::fs::write(outside_b_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
// and a third file in the project's root that should not be included
|
||||
let script2_path = case.project_path("script2.py");
|
||||
std::fs::write(script2_path.as_std_path(), "print('Hello')")?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("script2.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
let src_a_file = case.system_file(&src_a).unwrap();
|
||||
let outside_b_file = case.system_file(&outside_b_path).unwrap();
|
||||
|
||||
// The file should not have been added to the project files
|
||||
case.assert_indexed_project_files([main_file, outside_a, outside_b_file, src_a_file]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -470,7 +646,7 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), foo_source);
|
||||
assert_eq!(&case.collect_project_files(), &[foo]);
|
||||
case.assert_indexed_project_files([foo]);
|
||||
|
||||
update_file(&foo_path, "print('Version 2')")?;
|
||||
|
||||
@@ -481,7 +657,7 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
assert_eq!(&case.collect_project_files(), &[foo]);
|
||||
case.assert_indexed_project_files([foo]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -495,7 +671,7 @@ fn deleted_file() -> anyhow::Result<()> {
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
|
||||
assert!(foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_project_files(), &[foo]);
|
||||
case.assert_indexed_project_files([foo]);
|
||||
|
||||
std::fs::remove_file(foo_path.as_std_path())?;
|
||||
|
||||
@@ -504,7 +680,7 @@ fn deleted_file() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_project_files(), &[] as &[File]);
|
||||
case.assert_indexed_project_files([]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -524,7 +700,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
|
||||
assert!(foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_project_files(), &[foo]);
|
||||
case.assert_indexed_project_files([foo]);
|
||||
|
||||
std::fs::rename(
|
||||
foo_path.as_std_path(),
|
||||
@@ -536,7 +712,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_project_files(), &[] as &[File]);
|
||||
case.assert_indexed_project_files([]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -554,7 +730,7 @@ fn move_file_to_project() -> anyhow::Result<()> {
|
||||
let foo_in_project = case.project_path("foo.py");
|
||||
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
assert_eq!(&case.collect_project_files(), &[bar]);
|
||||
case.assert_indexed_project_files([bar]);
|
||||
|
||||
std::fs::rename(foo_path.as_std_path(), foo_in_project.as_std_path())?;
|
||||
|
||||
@@ -565,7 +741,7 @@ fn move_file_to_project() -> anyhow::Result<()> {
|
||||
let foo_in_project = case.system_file(&foo_in_project)?;
|
||||
|
||||
assert!(foo_in_project.exists(case.db()));
|
||||
assert_eq!(&case.collect_project_files(), &[bar, foo_in_project]);
|
||||
case.assert_indexed_project_files([bar, foo_in_project]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -579,7 +755,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
|
||||
assert_eq!(case.collect_project_files(), [foo]);
|
||||
case.assert_indexed_project_files([foo]);
|
||||
|
||||
std::fs::rename(foo_path.as_std_path(), bar_path.as_std_path())?;
|
||||
|
||||
@@ -592,7 +768,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
let bar = case.system_file(&bar_path)?;
|
||||
|
||||
assert!(bar.exists(case.db()));
|
||||
assert_eq!(case.collect_project_files(), [bar]);
|
||||
case.assert_indexed_project_files([bar]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -618,7 +794,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
assert_eq!(sub_a_module, None);
|
||||
assert_eq!(case.collect_project_files(), &[bar]);
|
||||
case.assert_indexed_project_files([bar]);
|
||||
|
||||
let sub_new_path = case.project_path("sub");
|
||||
std::fs::rename(sub_original_path.as_std_path(), sub_new_path.as_std_path())
|
||||
@@ -642,7 +818,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
|
||||
)
|
||||
.is_some());
|
||||
|
||||
assert_eq!(case.collect_project_files(), &[bar, init_file, a_file]);
|
||||
case.assert_indexed_project_files([bar, init_file, a_file]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -670,7 +846,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
.system_file(sub_path.join("a.py"))
|
||||
.expect("a.py to exist");
|
||||
|
||||
assert_eq!(case.collect_project_files(), &[bar, init_file, a_file]);
|
||||
case.assert_indexed_project_files([bar, init_file, a_file]);
|
||||
|
||||
std::fs::create_dir(case.root_path().join(".trash").as_std_path())?;
|
||||
let trashed_sub = case.root_path().join(".trash/sub");
|
||||
@@ -691,7 +867,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
assert!(!init_file.exists(case.db()));
|
||||
assert!(!a_file.exists(case.db()));
|
||||
|
||||
assert_eq!(case.collect_project_files(), &[bar]);
|
||||
case.assert_indexed_project_files([bar]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -725,7 +901,7 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
.system_file(sub_path.join("a.py"))
|
||||
.expect("a.py to exist");
|
||||
|
||||
assert_eq!(case.collect_project_files(), &[bar, sub_init, sub_a]);
|
||||
case.assert_indexed_project_files([bar, sub_init, sub_a]);
|
||||
|
||||
let foo_baz = case.project_path("foo/baz");
|
||||
|
||||
@@ -767,10 +943,7 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
assert!(foo_baz_init.exists(case.db()));
|
||||
assert!(foo_baz_a.exists(case.db()));
|
||||
|
||||
assert_eq!(
|
||||
case.collect_project_files(),
|
||||
&[bar, foo_baz_init, foo_baz_a]
|
||||
);
|
||||
case.assert_indexed_project_files([bar, foo_baz_init, foo_baz_a]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -799,7 +972,7 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
let a_file = case
|
||||
.system_file(sub_path.join("a.py"))
|
||||
.expect("a.py to exist");
|
||||
assert_eq!(case.collect_project_files(), &[bar, init_file, a_file]);
|
||||
case.assert_indexed_project_files([bar, init_file, a_file]);
|
||||
|
||||
std::fs::remove_dir_all(sub_path.as_std_path())
|
||||
.with_context(|| "Failed to remove the sub directory")?;
|
||||
@@ -817,15 +990,17 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
assert!(!init_file.exists(case.db()));
|
||||
assert!(!a_file.exists(case.db()));
|
||||
assert_eq!(case.collect_project_files(), &[bar]);
|
||||
case.assert_indexed_project_files([bar]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
|
||||
Some(Options {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
context.write_project_file("bar.py", "import sub.a")?;
|
||||
|
||||
context.set_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
context.join_root_path("site_packages"),
|
||||
@@ -833,7 +1008,8 @@ fn search_path() -> anyhow::Result<()> {
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
})
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let site_packages = case.root_path().join("site_packages");
|
||||
@@ -850,10 +1026,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert_eq!(
|
||||
case.collect_project_files(),
|
||||
&[case.system_file(case.project_path("bar.py")).unwrap()]
|
||||
);
|
||||
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -890,8 +1063,9 @@ 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| {
|
||||
Some(Options {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
context.write_project_file("bar.py", "import sub.a")?;
|
||||
context.set_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
context.join_root_path("site_packages"),
|
||||
@@ -899,7 +1073,9 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Remove site packages from the search path settings.
|
||||
@@ -922,30 +1098,30 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn change_python_version_and_platform() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options(
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
// `sys.last_exc` is a Python 3.12 only feature
|
||||
// `os.getegid()` is Unix only
|
||||
[(
|
||||
context.write_project_file(
|
||||
"bar.py",
|
||||
r#"
|
||||
import sys
|
||||
import os
|
||||
print(sys.last_exc, os.getegid())
|
||||
"#,
|
||||
)],
|
||||
|_context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
|
||||
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
|
||||
"win32".to_string(),
|
||||
))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
})
|
||||
},
|
||||
)?;
|
||||
)?;
|
||||
context.set_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
|
||||
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
|
||||
"win32".to_string(),
|
||||
))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let diagnostics = case.db.check().context("Failed to check project.")?;
|
||||
|
||||
@@ -980,38 +1156,35 @@ print(sys.last_exc, os.getegid())
|
||||
|
||||
#[test]
|
||||
fn changed_versions_file() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options(
|
||||
|context: &SetupContext| {
|
||||
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(),
|
||||
"# not important",
|
||||
)?;
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
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(),
|
||||
"# not important",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
})
|
||||
},
|
||||
)?;
|
||||
context.set_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Unset the custom typeshed directory.
|
||||
assert_eq!(
|
||||
@@ -1056,7 +1229,7 @@ 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 mut case = setup(|context: &mut SetupContext| {
|
||||
let foo_path = context.join_project_path("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
@@ -1075,6 +1248,7 @@ fn hard_links_in_project() -> anyhow::Result<()> {
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
|
||||
case.assert_indexed_project_files([bar, foo]);
|
||||
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
@@ -1127,7 +1301,7 @@ 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 mut case = setup(|context: &mut SetupContext| {
|
||||
let foo_path = context.join_root_path("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
@@ -1235,7 +1409,7 @@ 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(|context: &mut SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let link_target = context.join_root_path("bar");
|
||||
std::fs::create_dir_all(link_target.as_std_path())
|
||||
@@ -1316,7 +1490,7 @@ mod unix {
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlink_inside_project() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let link_target = context.join_project_path("patched/bar");
|
||||
std::fs::create_dir_all(link_target.as_std_path())
|
||||
@@ -1354,6 +1528,8 @@ mod unix {
|
||||
);
|
||||
assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz));
|
||||
|
||||
case.assert_indexed_project_files([patched_bar_baz_file]);
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(&patched_bar_baz, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
@@ -1389,6 +1565,7 @@ mod unix {
|
||||
bar_baz_text = bar_baz_text.as_str()
|
||||
);
|
||||
|
||||
case.assert_indexed_project_files([patched_bar_baz_file]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1406,43 +1583,39 @@ mod unix {
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlinked_module_search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options(
|
||||
|context: &SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let site_packages = context.join_root_path("site-packages");
|
||||
let bar = site_packages.join("bar");
|
||||
std::fs::create_dir_all(bar.as_std_path())
|
||||
.context("Failed to create bar directory")?;
|
||||
let baz_original = bar.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.context("Failed to write baz.py")?;
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let site_packages = context.join_root_path("site-packages");
|
||||
let bar = site_packages.join("bar");
|
||||
std::fs::create_dir_all(bar.as_std_path()).context("Failed to create bar directory")?;
|
||||
let baz_original = bar.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.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");
|
||||
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
|
||||
.context("Failed to create .venv directory")?;
|
||||
std::os::unix::fs::symlink(
|
||||
site_packages.as_std_path(),
|
||||
venv_site_packages.as_std_path(),
|
||||
)
|
||||
.context("Failed to create symlink to site-packages")?;
|
||||
// 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");
|
||||
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
|
||||
.context("Failed to create .venv directory")?;
|
||||
std::os::unix::fs::symlink(
|
||||
site_packages.as_std_path(),
|
||||
venv_site_packages.as_std_path(),
|
||||
)
|
||||
.context("Failed to create symlink to site-packages")?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|_context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
".venv/lib/python3.12/site-packages",
|
||||
)]),
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
})
|
||||
},
|
||||
)?;
|
||||
context.set_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
".venv/lib/python3.12/site-packages",
|
||||
)]),
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
@@ -1469,6 +1642,8 @@ mod unix {
|
||||
Some(&*baz_original)
|
||||
);
|
||||
|
||||
case.assert_indexed_project_files([]);
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(&baz_original, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
@@ -1494,13 +1669,15 @@ mod unix {
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
case.assert_indexed_project_files([]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_projects_delete_root() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
std::fs::write(
|
||||
context.join_project_path("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
@@ -1542,7 +1719,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
|
||||
fn changes_to_user_configuration() -> anyhow::Result<()> {
|
||||
let mut _config_dir_override: Option<UserConfigDirectoryOverrideGuard> = None;
|
||||
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
std::fs::write(
|
||||
context.join_project_path("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashMap, hash::BuildHasher};
|
||||
|
||||
use red_knot_python_semantic::{PythonPlatform, SitePackages};
|
||||
use red_knot_python_semantic::{PythonPath, PythonPlatform};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
@@ -128,7 +128,7 @@ macro_rules! impl_noop_combine {
|
||||
|
||||
impl_noop_combine!(SystemPathBuf);
|
||||
impl_noop_combine!(PythonPlatform);
|
||||
impl_noop_combine!(SitePackages);
|
||||
impl_noop_combine!(PythonPath);
|
||||
impl_noop_combine!(PythonVersion);
|
||||
|
||||
// std types
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
@@ -55,11 +55,11 @@ impl ProjectDatabase {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
self.with_db(|db| db.project().check(db))
|
||||
}
|
||||
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
|
||||
|
||||
self.with_db(|db| self.project().check_file(db, file))
|
||||
|
||||
@@ -2,20 +2,20 @@ use crate::db::{Db, ProjectDatabase};
|
||||
use crate::metadata::options::Options;
|
||||
use crate::watch::{ChangeEvent, CreatedKind, DeletedKind};
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::walk::ProjectFilesWalker;
|
||||
use red_knot_python_semantic::Program;
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::system::walk_directory::WalkState;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::SystemPath;
|
||||
use ruff_db::Db as _;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
impl ProjectDatabase {
|
||||
#[tracing::instrument(level = "debug", skip(self, changes, cli_options))]
|
||||
pub fn apply_changes(&mut self, changes: Vec<ChangeEvent>, cli_options: Option<&Options>) {
|
||||
let mut project = self.project();
|
||||
let project_path = project.root(self).to_path_buf();
|
||||
let project_root = project.root(self).to_path_buf();
|
||||
let program = Program::get(self);
|
||||
let custom_stdlib_versions_path = program
|
||||
.custom_stdlib_search_path(self)
|
||||
@@ -30,7 +30,7 @@ impl ProjectDatabase {
|
||||
|
||||
// Deduplicate the `sync` calls. Many file watchers emit multiple events for the same path.
|
||||
let mut synced_files = FxHashSet::default();
|
||||
let mut synced_recursively = FxHashSet::default();
|
||||
let mut sync_recursively = BTreeSet::default();
|
||||
|
||||
let mut sync_path = |db: &mut ProjectDatabase, path: &SystemPath| {
|
||||
if synced_files.insert(path.to_path_buf()) {
|
||||
@@ -38,13 +38,9 @@ impl ProjectDatabase {
|
||||
}
|
||||
};
|
||||
|
||||
let mut sync_recursively = |db: &mut ProjectDatabase, path: &SystemPath| {
|
||||
if synced_recursively.insert(path.to_path_buf()) {
|
||||
Files::sync_recursively(db, path);
|
||||
}
|
||||
};
|
||||
|
||||
for change in changes {
|
||||
tracing::trace!("Handle change: {:?}", change);
|
||||
|
||||
if let Some(path) = change.system_path() {
|
||||
if matches!(
|
||||
path.file_name(),
|
||||
@@ -70,16 +66,27 @@ impl ProjectDatabase {
|
||||
match kind {
|
||||
CreatedKind::File => sync_path(self, &path),
|
||||
CreatedKind::Directory | CreatedKind::Any => {
|
||||
sync_recursively(self, &path);
|
||||
sync_recursively.insert(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if self.system().is_file(&path) {
|
||||
// Add the parent directory because `walkdir` always visits explicitly passed files
|
||||
// even if they match an exclude filter.
|
||||
added_paths.insert(path.parent().unwrap().to_path_buf());
|
||||
} else {
|
||||
added_paths.insert(path);
|
||||
// Unlike other files, it's not only important to update the status of existing
|
||||
// and known `File`s (`sync_recursively`), it's also important to discover new files
|
||||
// that were added in the project's root (or any of the paths included for checking).
|
||||
//
|
||||
// This is important because `Project::check` iterates over all included files.
|
||||
// The code below walks the `added_paths` and adds all files that
|
||||
// should be included in the project. We can skip this check for
|
||||
// paths that aren't part of the project or shouldn't be included
|
||||
// when checking the project.
|
||||
if project.is_path_included(self, &path) {
|
||||
if self.system().is_file(&path) {
|
||||
// Add the parent directory because `walkdir` always visits explicitly passed files
|
||||
// even if they match an exclude filter.
|
||||
added_paths.insert(path.parent().unwrap().to_path_buf());
|
||||
} else {
|
||||
added_paths.insert(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +110,7 @@ impl ProjectDatabase {
|
||||
project.remove_file(self, file);
|
||||
}
|
||||
} else {
|
||||
sync_recursively(self, &path);
|
||||
sync_recursively.insert(path.clone());
|
||||
|
||||
if custom_stdlib_versions_path
|
||||
.as_ref()
|
||||
@@ -112,11 +119,19 @@ impl ProjectDatabase {
|
||||
custom_stdlib_change = true;
|
||||
}
|
||||
|
||||
// Perform a full-reload in case the deleted directory contained the pyproject.toml.
|
||||
// We may want to make this more clever in the future, to e.g. iterate over the
|
||||
// indexed files and remove the once that start with the same path, unless
|
||||
// the deleted path is the project configuration.
|
||||
project_changed = true;
|
||||
if project.is_path_included(self, &path) || path == project_root {
|
||||
// TODO: Shouldn't it be enough to simply traverse the project files and remove all
|
||||
// that start with the given path?
|
||||
tracing::debug!(
|
||||
"Reload project because of a path that could have been a directory."
|
||||
);
|
||||
|
||||
// Perform a full-reload in case the deleted directory contained the pyproject.toml.
|
||||
// We may want to make this more clever in the future, to e.g. iterate over the
|
||||
// indexed files and remove the once that start with the same path, unless
|
||||
// the deleted path is the project configuration.
|
||||
project_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,13 +148,29 @@ impl ProjectDatabase {
|
||||
ChangeEvent::Rescan => {
|
||||
project_changed = true;
|
||||
Files::sync_all(self);
|
||||
sync_recursively.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sync_recursively = sync_recursively.into_iter();
|
||||
let mut last = None;
|
||||
|
||||
for path in sync_recursively {
|
||||
// Avoid re-syncing paths that are sub-paths of each other.
|
||||
if let Some(last) = &last {
|
||||
if path.starts_with(last) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Files::sync_recursively(self, &path);
|
||||
last = Some(path);
|
||||
}
|
||||
|
||||
if project_changed {
|
||||
match ProjectMetadata::discover(&project_path, self.system()) {
|
||||
match ProjectMetadata::discover(&project_root, self.system()) {
|
||||
Ok(mut metadata) => {
|
||||
if let Some(cli_options) = cli_options {
|
||||
metadata.apply_cli_options(cli_options.clone());
|
||||
@@ -186,50 +217,24 @@ impl ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
let mut added_paths = added_paths.into_iter();
|
||||
let diagnostics = if let Some(walker) = ProjectFilesWalker::incremental(self, added_paths) {
|
||||
// Use directory walking to discover newly added files.
|
||||
let (files, diagnostics) = walker.collect_vec(self);
|
||||
|
||||
// Use directory walking to discover newly added files.
|
||||
if let Some(path) = added_paths.next() {
|
||||
let mut walker = self.system().walk_directory(&path);
|
||||
|
||||
for extra_path in added_paths {
|
||||
walker = walker.add(&extra_path);
|
||||
for file in files {
|
||||
project.add_file(self, file);
|
||||
}
|
||||
|
||||
let added_paths = std::sync::Mutex::new(Vec::default());
|
||||
diagnostics
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
walker.run(|| {
|
||||
Box::new(|entry| {
|
||||
let Ok(entry) = entry else {
|
||||
return WalkState::Continue;
|
||||
};
|
||||
|
||||
if !entry.file_type().is_file() {
|
||||
return WalkState::Continue;
|
||||
}
|
||||
|
||||
if entry
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(PySourceType::try_from_extension)
|
||||
.is_some()
|
||||
{
|
||||
let mut paths = added_paths.lock().unwrap();
|
||||
|
||||
paths.push(entry.into_path());
|
||||
}
|
||||
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
for path in added_paths.into_inner().unwrap() {
|
||||
let file = system_path_to_file(self, &path);
|
||||
|
||||
if let Ok(file) = file {
|
||||
project.add_file(self, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: We simply replace all IO related diagnostics here. This isn't ideal, because
|
||||
// it removes IO errors that may still be relevant. However, tracking IO errors correctly
|
||||
// across revisions doesn't feel essential, considering that they're rare. However, we could
|
||||
// implement a `BTreeMap` or similar and only prune the diagnostics from paths that we've
|
||||
// re-scanned (or that were removed etc).
|
||||
project.replace_index_diagnostics(self, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ use salsa::Setter;
|
||||
use ruff_db::files::File;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::Project;
|
||||
|
||||
/// Cheap cloneable hash set of files.
|
||||
type FileSet = Arc<FxHashSet<File>>;
|
||||
use crate::{IOErrorDiagnostic, Project};
|
||||
|
||||
/// The indexed files of a project.
|
||||
///
|
||||
@@ -35,9 +32,9 @@ impl IndexedFiles {
|
||||
}
|
||||
}
|
||||
|
||||
fn indexed(files: FileSet) -> Self {
|
||||
fn indexed(inner: Arc<IndexedInner>) -> Self {
|
||||
Self {
|
||||
state: std::sync::Mutex::new(State::Indexed(files)),
|
||||
state: std::sync::Mutex::new(State::Indexed(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +43,8 @@ impl IndexedFiles {
|
||||
|
||||
match &*state {
|
||||
State::Lazy => Index::Lazy(LazyFiles { files: state }),
|
||||
State::Indexed(files) => Index::Indexed(Indexed {
|
||||
files: Arc::clone(files),
|
||||
State::Indexed(inner) => Index::Indexed(Indexed {
|
||||
inner: Arc::clone(inner),
|
||||
_lifetime: PhantomData,
|
||||
}),
|
||||
}
|
||||
@@ -94,7 +91,7 @@ impl IndexedFiles {
|
||||
Some(IndexedMut {
|
||||
db: Some(db),
|
||||
project,
|
||||
files: indexed,
|
||||
indexed,
|
||||
did_change: false,
|
||||
})
|
||||
}
|
||||
@@ -112,7 +109,7 @@ enum State {
|
||||
Lazy,
|
||||
|
||||
/// The files are indexed. Stores the known files of a package.
|
||||
Indexed(FileSet),
|
||||
Indexed(Arc<IndexedInner>),
|
||||
}
|
||||
|
||||
pub(super) enum Index<'db> {
|
||||
@@ -129,32 +126,48 @@ pub(super) struct LazyFiles<'db> {
|
||||
|
||||
impl<'db> LazyFiles<'db> {
|
||||
/// Sets the indexed files of a package to `files`.
|
||||
pub(super) fn set(mut self, files: FxHashSet<File>) -> Indexed<'db> {
|
||||
pub(super) fn set(
|
||||
mut self,
|
||||
files: FxHashSet<File>,
|
||||
diagnostics: Vec<IOErrorDiagnostic>,
|
||||
) -> Indexed<'db> {
|
||||
let files = Indexed {
|
||||
files: Arc::new(files),
|
||||
inner: Arc::new(IndexedInner { files, diagnostics }),
|
||||
_lifetime: PhantomData,
|
||||
};
|
||||
*self.files = State::Indexed(Arc::clone(&files.files));
|
||||
*self.files = State::Indexed(Arc::clone(&files.inner));
|
||||
files
|
||||
}
|
||||
}
|
||||
|
||||
/// The indexed files of a package.
|
||||
/// The indexed files of the project.
|
||||
///
|
||||
/// Note: This type is intentionally non-cloneable. Making it cloneable requires
|
||||
/// revisiting the locking behavior in [`IndexedFiles::indexed_mut`].
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Indexed<'db> {
|
||||
files: FileSet,
|
||||
inner: Arc<IndexedInner>,
|
||||
// Preserve the lifetime of `PackageFiles`.
|
||||
_lifetime: PhantomData<&'db ()>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IndexedInner {
|
||||
files: FxHashSet<File>,
|
||||
diagnostics: Vec<IOErrorDiagnostic>,
|
||||
}
|
||||
|
||||
impl Indexed<'_> {
|
||||
pub(super) fn diagnostics(&self) -> &[IOErrorDiagnostic] {
|
||||
&self.inner.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Indexed<'_> {
|
||||
type Target = FxHashSet<File>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.files
|
||||
&self.inner.files
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +178,7 @@ impl<'a> IntoIterator for &'a Indexed<'_> {
|
||||
type IntoIter = IndexedIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.files.iter().copied()
|
||||
self.inner.files.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,13 +189,13 @@ impl<'a> IntoIterator for &'a Indexed<'_> {
|
||||
pub(super) struct IndexedMut<'db> {
|
||||
db: Option<&'db mut dyn Db>,
|
||||
project: Project,
|
||||
files: FileSet,
|
||||
indexed: Arc<IndexedInner>,
|
||||
did_change: bool,
|
||||
}
|
||||
|
||||
impl IndexedMut<'_> {
|
||||
pub(super) fn insert(&mut self, file: File) -> bool {
|
||||
if self.files_mut().insert(file) {
|
||||
if self.inner_mut().files.insert(file) {
|
||||
self.did_change = true;
|
||||
true
|
||||
} else {
|
||||
@@ -191,7 +204,7 @@ impl IndexedMut<'_> {
|
||||
}
|
||||
|
||||
pub(super) fn remove(&mut self, file: File) -> bool {
|
||||
if self.files_mut().remove(&file) {
|
||||
if self.inner_mut().files.remove(&file) {
|
||||
self.did_change = true;
|
||||
true
|
||||
} else {
|
||||
@@ -199,8 +212,13 @@ impl IndexedMut<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn files_mut(&mut self) -> &mut FxHashSet<File> {
|
||||
Arc::get_mut(&mut self.files).expect("All references to `FilesSet` to have been dropped")
|
||||
pub(super) fn set_diagnostics(&mut self, diagnostics: Vec<IOErrorDiagnostic>) {
|
||||
self.inner_mut().diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
fn inner_mut(&mut self) -> &mut IndexedInner {
|
||||
Arc::get_mut(&mut self.indexed)
|
||||
.expect("All references to `FilesSet` should have been dropped")
|
||||
}
|
||||
|
||||
fn set_impl(&mut self) {
|
||||
@@ -208,16 +226,16 @@ impl IndexedMut<'_> {
|
||||
return;
|
||||
};
|
||||
|
||||
let files = Arc::clone(&self.files);
|
||||
let indexed = Arc::clone(&self.indexed);
|
||||
|
||||
if self.did_change {
|
||||
// If there are changes, set the new file_set to trigger a salsa revision change.
|
||||
self.project
|
||||
.set_file_set(db)
|
||||
.to(IndexedFiles::indexed(files));
|
||||
.to(IndexedFiles::indexed(indexed));
|
||||
} else {
|
||||
// The `indexed_mut` replaced the `state` with Lazy. Restore it back to the indexed state.
|
||||
*self.project.file_set(db).state.lock().unwrap() = State::Indexed(files);
|
||||
*self.project.file_set(db).state.lock().unwrap() = State::Indexed(indexed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,7 +255,7 @@ mod tests {
|
||||
use crate::files::Index;
|
||||
use crate::ProjectMetadata;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::system::{DbWithWritableSystem as _, SystemPathBuf};
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
#[test]
|
||||
@@ -252,7 +270,7 @@ mod tests {
|
||||
let file = system_path_to_file(&db, "test.py").unwrap();
|
||||
|
||||
let files = match project.file_set(&db).get() {
|
||||
Index::Lazy(lazy) => lazy.set(FxHashSet::from_iter([file])),
|
||||
Index::Lazy(lazy) => lazy.set(FxHashSet::from_iter([file]), Vec::new()),
|
||||
Index::Indexed(files) => files,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::ref_option)]
|
||||
|
||||
use crate::metadata::options::OptionDiagnostic;
|
||||
use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
|
||||
pub use db::{Db, ProjectDatabase};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
use metadata::settings::Settings;
|
||||
@@ -8,24 +9,24 @@ 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::files::{system_path_to_file, File};
|
||||
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, OldParseDiagnostic, Severity, Span};
|
||||
use ruff_db::files::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 rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use rustc_hash::FxHashSet;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod combine;
|
||||
|
||||
mod db;
|
||||
mod files;
|
||||
pub mod metadata;
|
||||
mod walk;
|
||||
pub mod watch;
|
||||
|
||||
pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock<LintRegistry> =
|
||||
@@ -71,6 +72,30 @@ pub struct Project {
|
||||
#[return_ref]
|
||||
pub settings: Settings,
|
||||
|
||||
/// The paths that should be included when checking this project.
|
||||
///
|
||||
/// The default (when this list is empty) is to include all files in the project root
|
||||
/// (that satisfy the configured include and exclude patterns).
|
||||
/// However, it's sometimes desired to only check a subset of the project, e.g. to see
|
||||
/// the diagnostics for a single file or a folder.
|
||||
///
|
||||
/// This list gets initialized by the paths passed to `knot check <paths>`
|
||||
///
|
||||
/// ## How is this different from `open_files`?
|
||||
///
|
||||
/// The `included_paths` is closely related to `open_files`. The only difference is that
|
||||
/// `open_files` is already a resolved set of files whereas `included_paths` is only a list of paths
|
||||
/// that are resolved to files by indexing them. The other difference is that
|
||||
/// new files added to any directory in `included_paths` will be indexed and added to the project
|
||||
/// whereas `open_files` needs to be updated manually (e.g. by the IDE).
|
||||
///
|
||||
/// In short, `open_files` is cheaper in contexts where the set of files is known, like
|
||||
/// in an IDE when the user only wants to check the open tabs. This could be modeled
|
||||
/// with `included_paths` too but it would require an explicit walk dir step that's simply unnecessary.
|
||||
#[default]
|
||||
#[return_ref]
|
||||
included_paths_list: Vec<SystemPathBuf>,
|
||||
|
||||
/// Diagnostics that were generated when resolving the project settings.
|
||||
#[return_ref]
|
||||
settings_diagnostics: Vec<OptionDiagnostic>,
|
||||
@@ -106,6 +131,16 @@ impl Project {
|
||||
self.settings(db).to_rules()
|
||||
}
|
||||
|
||||
/// Returns `true` if `path` is both part of the project and included (see `included_paths_list`).
|
||||
///
|
||||
/// Unlike [Self::files], this method does not respect `.gitignore` files. It only checks
|
||||
/// the project's include and exclude settings as well as the paths that were passed to `knot check <paths>`.
|
||||
/// This means, that this method is an over-approximation of `Self::files` and may return `true` for paths
|
||||
/// that won't be included when checking the project because they're ignored in a `.gitignore` file.
|
||||
pub fn is_path_included(self, db: &dyn Db, path: &SystemPath) -> bool {
|
||||
ProjectFilesFilter::from_project(db, self).is_included(path)
|
||||
}
|
||||
|
||||
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
|
||||
tracing::debug!("Reloading project");
|
||||
assert_eq!(self.root(db), metadata.root());
|
||||
@@ -128,15 +163,22 @@ impl Project {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
let files = ProjectFiles::new(db, self);
|
||||
|
||||
diagnostics.extend(files.diagnostics().iter().cloned().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic);
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
@@ -147,7 +189,6 @@ impl Project {
|
||||
let project_span = project_span.clone();
|
||||
|
||||
rayon::scope(move |scope| {
|
||||
let files = ProjectFiles::new(&db, self);
|
||||
for file in &files {
|
||||
let result = inner_result.clone();
|
||||
let db = db.clone();
|
||||
@@ -166,12 +207,12 @@ impl Project {
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut file_diagnostics: Vec<_> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
})
|
||||
.collect();
|
||||
@@ -207,6 +248,30 @@ impl Project {
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn set_included_paths(self, db: &mut dyn Db, paths: Vec<SystemPathBuf>) {
|
||||
tracing::debug!("Setting included paths: {paths}", paths = paths.len());
|
||||
|
||||
self.set_included_paths_list(db).to(paths);
|
||||
self.reload_files(db);
|
||||
}
|
||||
|
||||
/// Returns the paths that should be checked.
|
||||
///
|
||||
/// The default is to check the entire project in which case this method returns
|
||||
/// the project root. However, users can specify to only check specific sub-folders or
|
||||
/// even files of a project by using `knot check <paths>`. In that case, this method
|
||||
/// returns the provided absolute paths.
|
||||
///
|
||||
/// Note: The CLI doesn't prohibit users from specifying paths outside the project root.
|
||||
/// This can be useful to check arbitrary files, but it isn't something we recommend.
|
||||
/// We should try to support this use case but it's okay if there are some limitations around it.
|
||||
fn included_paths_or_root(self, db: &dyn Db) -> &[SystemPathBuf] {
|
||||
match &**self.included_paths_list(db) {
|
||||
[] => std::slice::from_ref(&self.metadata(db).root),
|
||||
paths => paths,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the open files in the project or `None` if the entire project should be checked.
|
||||
pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet<File>> {
|
||||
self.open_fileset(db).as_deref()
|
||||
@@ -289,6 +354,17 @@ impl Project {
|
||||
index.insert(file);
|
||||
}
|
||||
|
||||
/// Replaces the diagnostics from indexing the project files with `diagnostics`.
|
||||
///
|
||||
/// This is a no-op if the project files haven't been indexed yet.
|
||||
pub fn replace_index_diagnostics(self, db: &mut dyn Db, diagnostics: Vec<IOErrorDiagnostic>) {
|
||||
let Some(mut index) = IndexedFiles::indexed_mut(db, self) else {
|
||||
return;
|
||||
};
|
||||
|
||||
index.set_diagnostics(diagnostics);
|
||||
}
|
||||
|
||||
/// Returns the files belonging to this project.
|
||||
pub fn files(self, db: &dyn Db) -> Indexed<'_> {
|
||||
let files = self.file_set(db);
|
||||
@@ -296,12 +372,14 @@ impl Project {
|
||||
let indexed = match files.get() {
|
||||
Index::Lazy(vacant) => {
|
||||
let _entered =
|
||||
tracing::debug_span!("Project::index_files", package = %self.name(db))
|
||||
tracing::debug_span!("Project::index_files", project = %self.name(db))
|
||||
.entered();
|
||||
|
||||
let files = discover_project_files(db, self);
|
||||
tracing::info!("Found {} files in project `{}`", files.len(), self.name(db));
|
||||
vacant.set(files)
|
||||
let walker = ProjectFilesWalker::new(db);
|
||||
let (files, diagnostics) = walker.collect_set(db);
|
||||
|
||||
tracing::info!("Indexed {} file(s)", files.len());
|
||||
vacant.set(files, diagnostics)
|
||||
}
|
||||
Index::Indexed(indexed) => indexed,
|
||||
};
|
||||
@@ -319,28 +397,29 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(Box::new(IOErrorDiagnostic {
|
||||
file,
|
||||
error: read_error.clone(),
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|error| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(ParseDiagnostic::new(file, error.clone()));
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> =
|
||||
Box::new(OldParseDiagnostic::new(file, error.clone()));
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
let boxed: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
|
||||
@@ -355,53 +434,6 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn discover_project_files(db: &dyn Db, project: Project) -> FxHashSet<File> {
|
||||
let paths = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
db.system().walk_directory(project.root(db)).run(|| {
|
||||
Box::new(|entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
// Skip over any non python files to avoid creating too many entries in `Files`.
|
||||
match entry.file_type() {
|
||||
FileType::File => {
|
||||
if entry
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(PySourceType::try_from_extension)
|
||||
.is_some()
|
||||
{
|
||||
let mut paths = paths.lock().unwrap();
|
||||
paths.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
FileType::Directory | FileType::Symlink => {}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// TODO Handle error
|
||||
tracing::error!("Failed to walk path: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
let paths = paths.into_inner().unwrap();
|
||||
let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher);
|
||||
|
||||
for path in paths {
|
||||
// If this returns `None`, then the file was deleted between the `walk_directory` call and now.
|
||||
// We can ignore this.
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), &path) {
|
||||
files.insert(file);
|
||||
}
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProjectFiles<'a> {
|
||||
OpenFiles(&'a FxHashSet<File>),
|
||||
@@ -416,6 +448,13 @@ impl<'a> ProjectFiles<'a> {
|
||||
ProjectFiles::Indexed(project.files(db))
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostics(&self) -> &[IOErrorDiagnostic] {
|
||||
match self {
|
||||
ProjectFiles::OpenFiles(_) => &[],
|
||||
ProjectFiles::Indexed(indexed) => indexed.diagnostics(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a ProjectFiles<'a> {
|
||||
@@ -448,13 +487,13 @@ impl Iterator for ProjectFilesIter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IOErrorDiagnostic {
|
||||
file: File,
|
||||
error: SourceTextError,
|
||||
file: Option<File>,
|
||||
error: IOErrorKind,
|
||||
}
|
||||
|
||||
impl Diagnostic for IOErrorDiagnostic {
|
||||
impl OldDiagnosticTrait for IOErrorDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
@@ -464,7 +503,7 @@ impl Diagnostic for IOErrorDiagnostic {
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file))
|
||||
self.file.map(Span::from)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
@@ -472,15 +511,24 @@ impl Diagnostic for IOErrorDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
enum IOErrorKind {
|
||||
#[error(transparent)]
|
||||
Walk(#[from] walk::WalkError),
|
||||
|
||||
#[error(transparent)]
|
||||
SourceText(#[from] SourceTextError),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file_impl, ProjectMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
|
||||
@@ -77,10 +77,10 @@ impl ProjectMetadata {
|
||||
// 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
|
||||
if options
|
||||
.environment
|
||||
.as_ref()
|
||||
.is_some_and(|env| env.python_version.is_some())
|
||||
.is_none_or(|env| env.python_version.is_none())
|
||||
{
|
||||
if let Some(requires_python) = project.resolve_requires_python_lower_bound()? {
|
||||
let mut environment = options.environment.unwrap_or_default();
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([(root.join("foo.py"), ""), (root.join("bar.py"), "")])
|
||||
.write_files_all([(root.join("foo.py"), ""), (root.join("bar.py"), "")])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let project =
|
||||
@@ -349,7 +349,7 @@ mod tests {
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -393,7 +393,7 @@ mod tests {
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -432,7 +432,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -482,7 +482,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -532,7 +532,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -572,7 +572,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -623,7 +623,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
.write_files_all([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
@@ -673,7 +673,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -703,7 +703,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -735,7 +735,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -765,7 +765,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -795,7 +795,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -828,7 +828,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -861,7 +861,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -886,7 +886,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
@@ -911,7 +911,7 @@ expected `.`, `]`
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_file(
|
||||
.write_file_all(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
@@ -90,7 +90,7 @@ impl Options {
|
||||
.map(|env| {
|
||||
(
|
||||
env.extra_paths.clone(),
|
||||
env.venv_path.clone(),
|
||||
env.python.clone(),
|
||||
env.typeshed.clone(),
|
||||
)
|
||||
})
|
||||
@@ -104,11 +104,11 @@ impl Options {
|
||||
.collect(),
|
||||
src_roots,
|
||||
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
|
||||
site_packages: python
|
||||
.map(|venv_path| SitePackages::Derived {
|
||||
venv_path: venv_path.absolute(project_root, system),
|
||||
python_path: python
|
||||
.map(|python_path| {
|
||||
PythonPath::SysPrefix(python_path.absolute(project_root, system))
|
||||
})
|
||||
.unwrap_or(SitePackages::Known(vec![])),
|
||||
.unwrap_or(PythonPath::KnownSitePackages(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,10 +236,14 @@ pub struct EnvironmentOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub typeshed: Option<RelativePathBuf>,
|
||||
|
||||
// TODO: Rename to python, see https://github.com/astral-sh/ruff/issues/15530
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
|
||||
///
|
||||
/// Red Knot will search in the path's `site-packages` directories for type information and
|
||||
/// third-party imports.
|
||||
///
|
||||
/// This option is commonly used to specify the path to a virtual environment.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub venv_path: Option<RelativePathBuf>,
|
||||
pub python: Option<RelativePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
@@ -372,7 +376,7 @@ impl OptionDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for OptionDiagnostic {
|
||||
impl OldDiagnosticTrait for OptionDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
256
crates/red_knot_project/src/walk.rs
Normal file
256
crates/red_knot_project/src/walk.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use crate::{Db, IOErrorDiagnostic, IOErrorKind, Project};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::walk_directory::{ErrorKind, WalkDirectoryBuilder, WalkState};
|
||||
use ruff_db::system::{FileType, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Filter that decides which files are included in the project.
|
||||
///
|
||||
/// In the future, this will hold a reference to the `include` and `exclude` pattern.
|
||||
///
|
||||
/// This struct mainly exists because `dyn Db` isn't `Send` or `Sync`, making it impossible
|
||||
/// to access fields from within the walker.
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct ProjectFilesFilter<'a> {
|
||||
/// The same as [`Project::included_paths_or_root`].
|
||||
included_paths: &'a [SystemPathBuf],
|
||||
|
||||
/// The filter skips checking if the path is in `included_paths` if set to `true`.
|
||||
///
|
||||
/// Skipping this check is useful when the walker only walks over `included_paths`.
|
||||
skip_included_paths: bool,
|
||||
}
|
||||
|
||||
impl<'a> ProjectFilesFilter<'a> {
|
||||
pub(crate) fn from_project(db: &'a dyn Db, project: Project) -> Self {
|
||||
Self {
|
||||
included_paths: project.included_paths_or_root(db),
|
||||
skip_included_paths: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a file is part of the project and included in the paths to check.
|
||||
///
|
||||
/// A file is included in the checked files if it is a sub path of the project's root
|
||||
/// (when no CLI path arguments are specified) or if it is a sub path of any path provided on the CLI (`knot check <paths>`) AND:
|
||||
///
|
||||
/// * It matches a positive `include` pattern and isn't excluded by a later negative `include` pattern.
|
||||
/// * It doesn't match a positive `exclude` pattern or is re-included by a later negative `exclude` pattern.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// This method may return `true` for files that don't end up being included when walking the
|
||||
/// project tree because it doesn't consider `.gitignore` and other ignore files when deciding
|
||||
/// if a file's included.
|
||||
pub(crate) fn is_included(&self, path: &SystemPath) -> bool {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum CheckPathMatch {
|
||||
/// The path is a partial match of the checked path (it's a sub path)
|
||||
Partial,
|
||||
|
||||
/// The path matches a check path exactly.
|
||||
Full,
|
||||
}
|
||||
|
||||
let m = if self.skip_included_paths {
|
||||
Some(CheckPathMatch::Partial)
|
||||
} else {
|
||||
self.included_paths
|
||||
.iter()
|
||||
.filter_map(|included_path| {
|
||||
if let Ok(relative_path) = path.strip_prefix(included_path) {
|
||||
// Exact matches are always included
|
||||
if relative_path.as_str().is_empty() {
|
||||
Some(CheckPathMatch::Full)
|
||||
} else {
|
||||
Some(CheckPathMatch::Partial)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max()
|
||||
};
|
||||
|
||||
match m {
|
||||
None => false,
|
||||
Some(CheckPathMatch::Partial) => {
|
||||
// TODO: For partial matches, only include the file if it is included by the project's include/exclude settings.
|
||||
true
|
||||
}
|
||||
Some(CheckPathMatch::Full) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ProjectFilesWalker<'a> {
|
||||
walker: WalkDirectoryBuilder,
|
||||
|
||||
filter: ProjectFilesFilter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ProjectFilesWalker<'a> {
|
||||
pub(crate) fn new(db: &'a dyn Db) -> Self {
|
||||
let project = db.project();
|
||||
|
||||
let mut filter = ProjectFilesFilter::from_project(db, project);
|
||||
// It's unnecessary to filter on included paths because it only iterates over those to start with.
|
||||
filter.skip_included_paths = true;
|
||||
|
||||
Self::from_paths(db, project.included_paths_or_root(db), filter)
|
||||
.expect("included_paths_or_root to never return an empty iterator")
|
||||
}
|
||||
|
||||
/// Creates a walker for indexing the project files incrementally.
|
||||
///
|
||||
/// The main difference to a full project walk is that `paths` may contain paths
|
||||
/// that aren't part of the included files.
|
||||
pub(crate) fn incremental<P>(db: &'a dyn Db, paths: impl IntoIterator<Item = P>) -> Option<Self>
|
||||
where
|
||||
P: AsRef<SystemPath>,
|
||||
{
|
||||
let project = db.project();
|
||||
|
||||
let filter = ProjectFilesFilter::from_project(db, project);
|
||||
|
||||
Self::from_paths(db, paths, filter)
|
||||
}
|
||||
|
||||
fn from_paths<P>(
|
||||
db: &'a dyn Db,
|
||||
paths: impl IntoIterator<Item = P>,
|
||||
filter: ProjectFilesFilter<'a>,
|
||||
) -> Option<Self>
|
||||
where
|
||||
P: AsRef<SystemPath>,
|
||||
{
|
||||
let mut paths = paths.into_iter();
|
||||
|
||||
let mut walker = db.system().walk_directory(paths.next()?.as_ref());
|
||||
|
||||
for path in paths {
|
||||
walker = walker.add(path);
|
||||
}
|
||||
|
||||
Some(Self { walker, filter })
|
||||
}
|
||||
|
||||
/// Walks the project paths and collects the paths of all files that
|
||||
/// are included in the project.
|
||||
pub(crate) fn walk_paths(self) -> (Vec<SystemPathBuf>, Vec<IOErrorDiagnostic>) {
|
||||
let paths = std::sync::Mutex::new(Vec::new());
|
||||
let diagnostics = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
self.walker.run(|| {
|
||||
Box::new(|entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
if !self.filter.is_included(entry.path()) {
|
||||
tracing::debug!("Ignoring not-included path: {}", entry.path());
|
||||
return WalkState::Skip;
|
||||
}
|
||||
|
||||
// Skip over any non python files to avoid creating too many entries in `Files`.
|
||||
match entry.file_type() {
|
||||
FileType::File => {
|
||||
if entry
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(PySourceType::try_from_extension)
|
||||
.is_some()
|
||||
{
|
||||
let mut paths = paths.lock().unwrap();
|
||||
paths.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
FileType::Directory | FileType::Symlink => {}
|
||||
}
|
||||
}
|
||||
Err(error) => match error.kind() {
|
||||
ErrorKind::Loop { .. } => {
|
||||
unreachable!("Loops shouldn't be possible without following symlinks.")
|
||||
}
|
||||
ErrorKind::Io { path, err } => {
|
||||
let mut diagnostics = diagnostics.lock().unwrap();
|
||||
let error = if let Some(path) = path {
|
||||
WalkError::IOPathError {
|
||||
path: path.clone(),
|
||||
error: err.to_string(),
|
||||
}
|
||||
} else {
|
||||
WalkError::IOError {
|
||||
error: err.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
diagnostics.push(IOErrorDiagnostic {
|
||||
file: None,
|
||||
error: IOErrorKind::Walk(error),
|
||||
});
|
||||
}
|
||||
ErrorKind::NonUtf8Path { path } => {
|
||||
diagnostics.lock().unwrap().push(IOErrorDiagnostic {
|
||||
file: None,
|
||||
error: IOErrorKind::Walk(WalkError::NonUtf8Path {
|
||||
path: path.clone(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
(
|
||||
paths.into_inner().unwrap(),
|
||||
diagnostics.into_inner().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
|
||||
let (paths, diagnostics) = self.walk_paths();
|
||||
|
||||
(
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(move |path| {
|
||||
// If this returns `None`, then the file was deleted between the `walk_directory` call and now.
|
||||
// We can ignore this.
|
||||
system_path_to_file(db.upcast(), &path).ok()
|
||||
})
|
||||
.collect(),
|
||||
diagnostics,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_set(self, db: &dyn Db) -> (FxHashSet<File>, Vec<IOErrorDiagnostic>) {
|
||||
let (paths, diagnostics) = self.walk_paths();
|
||||
|
||||
let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher);
|
||||
|
||||
for path in paths {
|
||||
if let Ok(file) = system_path_to_file(db.upcast(), &path) {
|
||||
files.insert(file);
|
||||
}
|
||||
}
|
||||
|
||||
(files, diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub(crate) enum WalkError {
|
||||
#[error("`{path}`: {error}")]
|
||||
IOPathError { path: SystemPathBuf, error: String },
|
||||
|
||||
#[error("Failed to walk project directory: {error}")]
|
||||
IOError { error: String },
|
||||
|
||||
#[error("`{path}` is not a valid UTF-8 path")]
|
||||
NonUtf8Path { path: PathBuf },
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use tracing::info;
|
||||
use red_knot_python_semantic::system_module_search_paths;
|
||||
use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_db::{Db as _, Upcast};
|
||||
use ruff_db::Upcast;
|
||||
|
||||
use crate::db::{Db, ProjectDatabase};
|
||||
use crate::watch::Watcher;
|
||||
@@ -42,9 +42,9 @@ impl ProjectWatcher {
|
||||
|
||||
pub fn update(&mut self, db: &ProjectDatabase) {
|
||||
let search_paths: Vec<_> = system_module_search_paths(db.upcast()).collect();
|
||||
let project_path = db.project().root(db).to_path_buf();
|
||||
let project_path = db.project().root(db);
|
||||
|
||||
let new_cache_key = Self::compute_cache_key(&project_path, &search_paths);
|
||||
let new_cache_key = Self::compute_cache_key(project_path, &search_paths);
|
||||
|
||||
if self.cache_key == Some(new_cache_key) {
|
||||
return;
|
||||
@@ -68,41 +68,47 @@ impl ProjectWatcher {
|
||||
|
||||
self.has_errored_paths = false;
|
||||
|
||||
let project_path = db
|
||||
.system()
|
||||
.canonicalize_path(&project_path)
|
||||
.unwrap_or(project_path);
|
||||
|
||||
let config_paths = db
|
||||
.project()
|
||||
.metadata(db)
|
||||
.extra_configuration_paths()
|
||||
.iter()
|
||||
.cloned();
|
||||
.map(SystemPathBuf::as_path);
|
||||
|
||||
// Watch both the project root and any paths provided by the user on the CLI (removing any redundant nested paths).
|
||||
// This is necessary to observe changes to files that are outside the project root.
|
||||
// We always need to watch the project root to observe changes to its configuration.
|
||||
let included_paths = ruff_db::system::deduplicate_nested_paths(
|
||||
std::iter::once(project_path).chain(
|
||||
db.project()
|
||||
.included_paths_list(db)
|
||||
.iter()
|
||||
.map(SystemPathBuf::as_path),
|
||||
),
|
||||
);
|
||||
|
||||
// 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(
|
||||
search_paths
|
||||
.into_iter()
|
||||
.filter(|path| !path.starts_with(&project_path)),
|
||||
)
|
||||
.map(SystemPath::to_path_buf);
|
||||
.filter(|path| !path.starts_with(project_path)),
|
||||
);
|
||||
|
||||
// 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)
|
||||
for path in included_paths
|
||||
.chain(unique_module_paths)
|
||||
.chain(config_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) {
|
||||
if let Err(error) = self.watcher.watch(path) {
|
||||
// TODO: Log a user-facing warning.
|
||||
tracing::warn!("Failed to setup watcher for path `{path}`: {error}. You have to restart Ruff after making changes to files under this path or you might see stale results.");
|
||||
self.has_errored_paths = true;
|
||||
} else {
|
||||
self.watched_paths.push(path);
|
||||
self.watched_paths.push(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
|
||||
let code = std::fs::read_to_string(source)?;
|
||||
|
||||
let mut check_with_file_name = |path: &SystemPath| {
|
||||
memory_fs.write_file(path, &code).unwrap();
|
||||
memory_fs.write_file_all(path, &code).unwrap();
|
||||
File::sync_path(&mut db, path);
|
||||
|
||||
// this test is only asserting that we can pull every expression type without a panic
|
||||
@@ -283,4 +283,9 @@ const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
||||
// related to circular references in f-string annotations (invalid syntax)
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true),
|
||||
// related to circular references in stub type annotations (salsa cycle panic):
|
||||
("crates/ruff_linter/resources/test/fixtures/pycodestyle/E501_4.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_0.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_12.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_14.py", false, true),
|
||||
];
|
||||
|
||||
@@ -42,6 +42,8 @@ smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
strum = { workspace = true}
|
||||
strum_macros = { workspace = true}
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["testing", "os"] }
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Tests for invalid types in type expressions
|
||||
|
||||
## Invalid types are rejected
|
||||
|
||||
Many types are illegal in the context of a type expression:
|
||||
|
||||
```py
|
||||
import typing
|
||||
from knot_extensions import AlwaysTruthy, AlwaysFalsy
|
||||
from typing_extensions import Literal, Never
|
||||
|
||||
def _(
|
||||
a: type[int],
|
||||
b: AlwaysTruthy,
|
||||
c: AlwaysFalsy,
|
||||
d: Literal[True],
|
||||
e: Literal["bar"],
|
||||
f: Literal[b"foo"],
|
||||
g: tuple[int, str],
|
||||
h: Never,
|
||||
):
|
||||
def foo(): ...
|
||||
def invalid(
|
||||
i: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
|
||||
j: b, # error: [invalid-type-form]
|
||||
k: c, # error: [invalid-type-form]
|
||||
l: d, # error: [invalid-type-form]
|
||||
m: e, # error: [invalid-type-form]
|
||||
n: f, # error: [invalid-type-form]
|
||||
o: g, # error: [invalid-type-form]
|
||||
p: h, # error: [invalid-type-form]
|
||||
q: typing, # error: [invalid-type-form]
|
||||
r: foo, # error: [invalid-type-form]
|
||||
):
|
||||
reveal_type(i) # revealed: Unknown
|
||||
reveal_type(j) # revealed: Unknown
|
||||
reveal_type(k) # revealed: Unknown
|
||||
reveal_type(l) # revealed: Unknown
|
||||
reveal_type(m) # revealed: Unknown
|
||||
reveal_type(n) # revealed: Unknown
|
||||
reveal_type(o) # revealed: Unknown
|
||||
reveal_type(p) # revealed: Unknown
|
||||
reveal_type(q) # revealed: Unknown
|
||||
reveal_type(r) # revealed: Unknown
|
||||
```
|
||||
@@ -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(overloaded method)
|
||||
|
||||
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(overloaded method)
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
@@ -116,8 +116,8 @@ MyType = int
|
||||
class Aliases:
|
||||
MyType = str
|
||||
|
||||
forward: "MyType"
|
||||
not_forward: MyType
|
||||
forward: "MyType" = "value"
|
||||
not_forward: MyType = "value"
|
||||
|
||||
reveal_type(Aliases.forward) # revealed: str
|
||||
reveal_type(Aliases.not_forward) # revealed: str
|
||||
|
||||
@@ -18,7 +18,7 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(Alias) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`)
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
@@ -33,7 +33,7 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
|
||||
|
||||
class Foo:
|
||||
def method(self, x: Self):
|
||||
reveal_type(x) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(x) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
@@ -54,13 +54,12 @@ c_instance.declared_and_bound = False
|
||||
# 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: we already show an error here but the message might be improved?
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
# error: [unresolved-attribute] "Type `Literal[C]` has no attribute `inferred_from_value`"
|
||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error (pure instance variables cannot be accessed on the class)
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`"
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
|
||||
# This assignment is fine:
|
||||
@@ -90,13 +89,13 @@ c_instance = C()
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: str | None
|
||||
|
||||
# TODO: we currently plan to emit a diagnostic here. Note that both mypy
|
||||
# and pyright show no error in this case! So we may reconsider this in
|
||||
# the future, if it turns out to produce too many false positives.
|
||||
reveal_type(C.declared_and_bound) # revealed: str | None
|
||||
# Note that both mypy and pyright show no error in this case! So we may reconsider this in
|
||||
# the future, if it turns out to produce too many false positives. We currently emit:
|
||||
# error: [unresolved-attribute] "Attribute `declared_and_bound` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
reveal_type(C.declared_and_bound) # revealed: Unknown
|
||||
|
||||
# TODO: same as above. We plan to emit a diagnostic here, even if both mypy
|
||||
# and pyright allow this.
|
||||
# Same as above. Mypy and pyright do not show an error here.
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `declared_and_bound` from the class object `Literal[C]`"
|
||||
C.declared_and_bound = "overwritten on class"
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`"
|
||||
@@ -116,11 +115,11 @@ c_instance = C()
|
||||
|
||||
reveal_type(c_instance.only_declared) # revealed: str
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit a diagnostic.
|
||||
# The type could be changed to 'Unknown' if we decide to emit an error?
|
||||
reveal_type(C.only_declared) # revealed: str
|
||||
# Mypy and pyright do not show an error here. We treat this as a pure instance variable.
|
||||
# error: [unresolved-attribute] "Attribute `only_declared` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
reveal_type(C.only_declared) # revealed: Unknown
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit one.
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `only_declared` from the class object `Literal[C]`"
|
||||
C.only_declared = "overwritten on class"
|
||||
```
|
||||
|
||||
@@ -191,11 +190,10 @@ reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||
|
||||
# TODO: We already show an error here, but the message might be improved?
|
||||
# error: [unresolved-attribute]
|
||||
# error: [unresolved-attribute] "Attribute `inferred_from_value` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `inferred_from_value` from the class object `Literal[C]`"
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
```
|
||||
|
||||
@@ -598,6 +596,9 @@ C.class_method()
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
# TODO: should be no error when descriptor protocol is supported
|
||||
# and the assignment is properly attributed to the class method.
|
||||
# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `Literal[C]`"
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
|
||||
# TODO: should be `Unknown | Literal["value set in class method"]` or
|
||||
@@ -782,6 +783,9 @@ def _(flag1: bool, flag2: bool):
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
@@ -805,6 +809,28 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
|
||||
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
|
||||
# see the "Possibly unbound/undeclared instance attribute" section below.
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within gradual types
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def _(flag: bool):
|
||||
class Base:
|
||||
x: Any
|
||||
|
||||
class Derived(Base):
|
||||
if flag:
|
||||
# Redeclaring `x` with a more static type is okay in terms of LSP.
|
||||
x: int
|
||||
|
||||
reveal_type(Derived().x) # revealed: int | Any
|
||||
```
|
||||
|
||||
### Attribute possibly unbound on a subclass but not on a superclass
|
||||
@@ -819,6 +845,8 @@ def _(flag: bool):
|
||||
x = 2
|
||||
|
||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||
|
||||
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
|
||||
```
|
||||
|
||||
### Attribute possibly unbound on a subclass and on a superclass
|
||||
@@ -835,6 +863,41 @@ def _(flag: bool):
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
|
||||
```
|
||||
|
||||
### Possibly unbound/undeclared instance attribute
|
||||
|
||||
#### Possibly unbound and undeclared
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
x: int
|
||||
|
||||
def __init(self):
|
||||
if flag:
|
||||
self.x = 1
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Foo().x) # revealed: int
|
||||
```
|
||||
|
||||
#### Possibly unbound
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __init(self):
|
||||
if flag:
|
||||
self.x = 1
|
||||
|
||||
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
|
||||
# if we ever will (or want to)
|
||||
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
### Attribute access on `Any`
|
||||
@@ -884,13 +947,18 @@ def _(flag: bool):
|
||||
|
||||
## Objects of all types have a `__class__` method
|
||||
|
||||
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||
`type(x)`.
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
|
||||
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
|
||||
reveal_type(type(typing_extensions)) # revealed: Literal[ModuleType]
|
||||
|
||||
a = 42
|
||||
reveal_type(a.__class__) # revealed: Literal[int]
|
||||
reveal_type(type(a)) # revealed: Literal[int]
|
||||
|
||||
b = "42"
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
@@ -906,8 +974,13 @@ reveal_type(e.__class__) # revealed: Literal[tuple]
|
||||
|
||||
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
reveal_type(a.__class__) # revealed: type[int]
|
||||
reveal_type(type(a)) # revealed: type[int]
|
||||
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
reveal_type(type(b)) # revealed: Literal[str]
|
||||
|
||||
reveal_type(c.__class__) # revealed: type[int] | type[str]
|
||||
reveal_type(type(c)) # revealed: type[int] | type[str]
|
||||
|
||||
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
|
||||
# It would be incorrect to infer `Literal[type]` here,
|
||||
@@ -1032,8 +1105,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
|
||||
bools are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: @Todo(decorated method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(decorated method)
|
||||
reveal_type(True.__and__) # revealed: @Todo(overloaded method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(overloaded method)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1136,6 +1209,20 @@ class C:
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Accessing attributes on `Never`
|
||||
|
||||
Arbitrary attributes can be accessed on `Never` without emitting any errors:
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
|
||||
def f(never: Never):
|
||||
reveal_type(never.arbitrary_attribute) # revealed: Never
|
||||
|
||||
# Assigning `Never` to an attribute on `Never` is also allowed:
|
||||
never.another_attribute = never
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
|
||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||
|
||||
@@ -259,11 +259,17 @@ class A:
|
||||
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
|
||||
```
|
||||
|
||||
Note that we union with `Unknown` here because `__add__` is not declared. We do infer just `int` if
|
||||
the callable is declared:
|
||||
|
||||
```py
|
||||
class B2:
|
||||
__add__: A = A()
|
||||
|
||||
reveal_type(B2() + B2()) # revealed: int
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
@@ -306,7 +312,7 @@ reveal_type(1 + A()) # revealed: A
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type)
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -314,7 +320,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo(return type)
|
||||
reveal_type(() + A()) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
@@ -323,7 +329,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
@@ -351,6 +357,20 @@ class Y(Foo): ...
|
||||
reveal_type(X() + Y()) # revealed: int
|
||||
```
|
||||
|
||||
## Operations involving types with invalid `__bool__` methods
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
|
||||
@@ -51,9 +51,9 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||
reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: @Todo(return type)
|
||||
reveal_type(2**x) # revealed: @Todo(return type)
|
||||
reveal_type(x**x) # revealed: @Todo(return type)
|
||||
reveal_type(x**2) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(2**x) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(x**x) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Calling builtins
|
||||
|
||||
## `bool` with incorrect arguments
|
||||
|
||||
```py
|
||||
class NotBool:
|
||||
__bool__ = None
|
||||
|
||||
# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument.
|
||||
bool(1, 2)
|
||||
|
||||
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
|
||||
bool(NotBool())
|
||||
```
|
||||
|
||||
## Calls to `type()`
|
||||
|
||||
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
|
||||
tested more extensively in `crates/red_knot_python_semantic/resources/mdtest/attributes.md`,
|
||||
alongside the tests for the `__class__` attribute.)
|
||||
|
||||
```py
|
||||
reveal_type(type(1)) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
```py
|
||||
reveal_type(type("Foo", (), {})) # revealed: type
|
||||
```
|
||||
|
||||
Other numbers of arguments are invalid (TODO -- these should emit a diagnostic)
|
||||
|
||||
```py
|
||||
type("Foo", ())
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
@@ -82,7 +82,7 @@ class C:
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`"
|
||||
reveal_type(c("foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -96,7 +96,7 @@ class C:
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
|
||||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of bound method `__call__`; expected type `int`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
128
crates/red_knot_python_semantic/resources/mdtest/call/dunder.md
Normal file
128
crates/red_knot_python_semantic/resources/mdtest/call/dunder.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Dunder calls
|
||||
|
||||
## Introduction
|
||||
|
||||
This test suite explains and documents how dunder methods are looked up and called. Throughout the
|
||||
document, we use `__getitem__` as an example, but the same principles apply to other dunder methods.
|
||||
|
||||
Dunder methods are implicitly called when using certain syntax. For example, the index operator
|
||||
`obj[key]` calls the `__getitem__` method under the hood. Exactly *how* a dunder method is looked up
|
||||
and called works slightly different from regular methods. Dunder methods are not looked up on `obj`
|
||||
directly, but rather on `type(obj)`. But in many ways, they still *act* as if they were called on
|
||||
`obj` directly. If the `__getitem__` member of `type(obj)` is a descriptor, it is called with `obj`
|
||||
as the `instance` argument to `__get__`. A desugared version of `obj[key]` is roughly equivalent to
|
||||
`getitem_desugared(obj, key)` as defined below:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def find_name_in_mro(typ: type, name: str) -> Any:
|
||||
# See implementation in https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
|
||||
pass
|
||||
|
||||
def getitem_desugared(obj: object, key: object) -> object:
|
||||
getitem_callable = find_name_in_mro(type(obj), "__getitem__")
|
||||
if hasattr(getitem_callable, "__get__"):
|
||||
getitem_callable = getitem_callable.__get__(obj, type(obj))
|
||||
|
||||
return getitem_callable(key)
|
||||
```
|
||||
|
||||
In the following tests, we demonstrate that we implement this behavior correctly.
|
||||
|
||||
## Operating on class objects
|
||||
|
||||
If we invoke a dunder method on a class, it is looked up on the *meta* class, since any class is an
|
||||
instance of its metaclass:
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
def __getitem__(cls, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class DunderOnMetaClass(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(DunderOnMetaClass[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Operating on instances
|
||||
|
||||
When invoking a dunder method on an instance of a class, it is looked up on the class:
|
||||
|
||||
```py
|
||||
class ClassWithNormalDunder:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class_with_normal_dunder = ClassWithNormalDunder()
|
||||
|
||||
reveal_type(class_with_normal_dunder[0]) # revealed: str
|
||||
```
|
||||
|
||||
Which can be demonstrated by trying to attach a dunder method to an instance, which will not work:
|
||||
|
||||
```py
|
||||
def external_getitem(instance, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class ThisFails:
|
||||
def __init__(self):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
this_fails = ThisFails()
|
||||
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `ThisFails` with no `__getitem__` method"
|
||||
reveal_type(this_fails[0]) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, the attached dunder method *can* be called if accessed directly:
|
||||
|
||||
```py
|
||||
# TODO: `this_fails.__getitem__` is incorrectly treated as a bound method. This
|
||||
# should be fixed with https://github.com/astral-sh/ruff/issues/16367
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
## When the dunder is not a method
|
||||
|
||||
A dunder can also be a non-method callable:
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
def __call__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class ClassWithNonMethodDunder:
|
||||
__getitem__: SomeCallable = SomeCallable()
|
||||
|
||||
class_with_callable_dunder = ClassWithNonMethodDunder()
|
||||
|
||||
reveal_type(class_with_callable_dunder[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Dunders are looked up using the descriptor protocol
|
||||
|
||||
Here, we demonstrate that the descriptor protocol is invoked when looking up a dunder method. Note
|
||||
that the `instance` argument is on object of type `ClassWithDescriptorDunder`:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class SomeCallable:
|
||||
def __call__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance: ClassWithDescriptorDunder, owner: type[ClassWithDescriptorDunder]) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class ClassWithDescriptorDunder:
|
||||
__getitem__: Descriptor = Descriptor()
|
||||
|
||||
class_with_descriptor_dunder = ClassWithDescriptorDunder()
|
||||
|
||||
reveal_type(class_with_descriptor_dunder[0]) # revealed: str
|
||||
```
|
||||
@@ -44,7 +44,7 @@ def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo(return type)
|
||||
reveal_type(bar()) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
@@ -239,11 +239,11 @@ method_wrapper(None, C)
|
||||
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`"
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
|
||||
method_wrapper(None, 1)
|
||||
|
||||
# Passing `None` as the `owner` argument when `instance` is `None` is an
|
||||
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`); expected type `type`"
|
||||
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
|
||||
method_wrapper(None, None)
|
||||
|
||||
# Calling `__get__` without any arguments is an
|
||||
@@ -251,8 +251,130 @@ method_wrapper(None, None)
|
||||
method_wrapper()
|
||||
|
||||
# Calling `__get__` with too many positional arguments is an
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 2, got 3"
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3"
|
||||
method_wrapper(C(), C, "one too many")
|
||||
```
|
||||
|
||||
## `@classmethod`
|
||||
|
||||
### Basic
|
||||
|
||||
When a `@classmethod` attribute is accessed, it returns a bound method object, even when accessed on
|
||||
the class object itself:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
def f(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(C().f) # revealed: <bound method `f` of `type[C]`>
|
||||
```
|
||||
|
||||
The `cls` method argument is then implicitly passed as the first argument when calling the method:
|
||||
|
||||
```py
|
||||
reveal_type(C.f(1)) # revealed: str
|
||||
reveal_type(C().f(1)) # revealed: str
|
||||
```
|
||||
|
||||
When the class method is called incorrectly, we detect it:
|
||||
|
||||
```py
|
||||
C.f("incorrect") # error: [invalid-argument-type]
|
||||
C.f() # error: [missing-argument]
|
||||
C.f(1, 2) # error: [too-many-positional-arguments]
|
||||
```
|
||||
|
||||
If the `cls` parameter is wrongly annotated, we emit an error at the call site:
|
||||
|
||||
```py
|
||||
class D:
|
||||
@classmethod
|
||||
def f(cls: D):
|
||||
# This function is wrongly annotated, it should be `type[D]` instead of `D`
|
||||
pass
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[D]` cannot be assigned to parameter 1 (`cls`) of bound method `f`; expected type `D`"
|
||||
D.f()
|
||||
```
|
||||
|
||||
When a class method is accessed on a derived class, it is bound to that derived class:
|
||||
|
||||
```py
|
||||
class Derived(C):
|
||||
pass
|
||||
|
||||
reveal_type(Derived.f) # revealed: <bound method `f` of `Literal[Derived]`>
|
||||
reveal_type(Derived().f) # revealed: <bound method `f` of `type[Derived]`>
|
||||
|
||||
reveal_type(Derived.f(1)) # revealed: str
|
||||
reveal_type(Derived().f(1)) # revealed: str
|
||||
```
|
||||
|
||||
### Accessing the classmethod as a static member
|
||||
|
||||
Accessing a `@classmethod`-decorated function at runtime returns a `classmethod` object. We
|
||||
currently don't model this explicitly:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
def f(cls): ...
|
||||
|
||||
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
|
||||
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
```
|
||||
|
||||
But we correctly model how the `classmethod` descriptor works:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: <bound method `f` of `type[C]`>
|
||||
```
|
||||
|
||||
The `owner` argument takes precedence over the `instance` argument:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||
```
|
||||
|
||||
### Classmethods mixed with other decorators
|
||||
|
||||
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
|
||||
class method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def does_nothing[T](f: T) -> T:
|
||||
return f
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: We do not support decorators yet (only limited special cases). Eventually,
|
||||
# these should all return `str`:
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f1(1)) # revealed: @Todo(decorated method)
|
||||
|
||||
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f2(1)) # revealed: @Todo(decorated method)
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Never is callable
|
||||
|
||||
The type `Never` is callable with an arbitrary set of arguments. The result is always `Never`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
|
||||
def f(never: Never):
|
||||
reveal_type(never()) # revealed: Never
|
||||
reveal_type(never(1)) # revealed: Never
|
||||
reveal_type(never(1, "a", never, x=None)) # revealed: Never
|
||||
```
|
||||
@@ -56,6 +56,7 @@ def _(flag: bool, flag2: bool):
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# TODO we should mention all non-callable elements of the union
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
# revealed: int | Unknown
|
||||
reveal_type(f())
|
||||
@@ -108,3 +109,38 @@ def _(flag: bool):
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union of binding errors
|
||||
|
||||
```py
|
||||
def f1(): ...
|
||||
def f2(): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = f2
|
||||
|
||||
# TODO: we should show all errors from the union, not arbitrarily pick one union element
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## One not-callable, one wrong argument
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
def f1(): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = C()
|
||||
|
||||
# TODO: we should either show all union errors here, or prioritize the not-callable error
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -160,3 +160,45 @@ 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
|
||||
```
|
||||
|
||||
## Return type that doesn't implement `__bool__` correctly
|
||||
|
||||
`in` and `not in` operations will fail at runtime if the object on the right-hand side of the
|
||||
operation has a `__contains__` method that returns a type which is not convertible to `bool`. This
|
||||
is because of the way these operations are handled by the Python interpreter at runtime. If we
|
||||
assume that `y` is an object that has a `__contains__` method, the Python expression `x in y`
|
||||
desugars to a `contains(y, x)` call, where `contains` looks something like this:
|
||||
|
||||
```ignore
|
||||
def contains(y, x):
|
||||
return bool(type(y).__contains__(y, x))
|
||||
```
|
||||
|
||||
where the `bool()` conversion itself implicitly calls `__bool__` under the hood.
|
||||
|
||||
TODO: Ideally the message would explain to the user what's wrong. E.g,
|
||||
|
||||
```ignore
|
||||
error: [operator] cannot use `in` operator on object of type `WithContains`
|
||||
note: This is because the `in` operator implicitly calls `WithContains.__contains__`, but `WithContains.__contains__` is invalidly defined
|
||||
note: `WithContains.__contains__` is invalidly defined because it returns an instance of `NotBoolable`, which cannot be evaluated in a boolean context
|
||||
note: `NotBoolable` cannot be evaluated in a boolean context because its `__bool__` attribute is not callable
|
||||
```
|
||||
|
||||
It may also be more appropriate to use `unsupported-operator` as the error code.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
class WithContains:
|
||||
def __contains__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 in WithContains()
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 not in WithContains()
|
||||
```
|
||||
|
||||
@@ -345,3 +345,47 @@ def f(x: bool, y: int):
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
```
|
||||
|
||||
## Chained comparisons with objects that don't implement `__bool__` correctly
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Python implicitly calls `bool` on the comparison result of preceding elements (but not for the last
|
||||
element) of a chained comparison.
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
def __gt__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 < Comparable() < 20
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 < Comparable() < Comparable()
|
||||
|
||||
Comparable() < Comparable() # fine
|
||||
```
|
||||
|
||||
## Callables as comparison dunders
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class AlwaysTrue:
|
||||
def __call__(self, other: object) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class A:
|
||||
__eq__: AlwaysTrue = AlwaysTrue()
|
||||
__lt__: AlwaysTrue = AlwaysTrue()
|
||||
|
||||
reveal_type(A() == A()) # revealed: Literal[True]
|
||||
reveal_type(A() < A()) # revealed: Literal[True]
|
||||
reveal_type(A() > A()) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -334,3 +334,61 @@ reveal_type(a is not c) # revealed: Literal[True]
|
||||
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
|
||||
|
||||
// TODO
|
||||
|
||||
## Chained comparisons with elements that incorrectly implement `__bool__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
For an operation `A() < A()` to succeed at runtime, the `A.__lt__` method does not necessarily need
|
||||
to return an object that is convertible to a `bool`. However, the return type _does_ need to be
|
||||
convertible to a `bool` for the operation `A() < A() < A()` (a _chained_ comparison) to succeed.
|
||||
This is because `A() < A() < A()` desugars to something like this, which involves several implicit
|
||||
conversions to `bool`:
|
||||
|
||||
```ignore
|
||||
def compute_chained_comparison():
|
||||
a1 = A()
|
||||
a2 = A()
|
||||
first_comparison = a1 < a2
|
||||
return first_comparison and (a2 < A())
|
||||
```
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 5
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
def __gt__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
a = (1, Comparable())
|
||||
b = (1, Comparable())
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
a < b < b
|
||||
|
||||
a < b # fine
|
||||
```
|
||||
|
||||
## Equality with elements that incorrectly implement `__bool__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Python does not generally attempt to coerce the result of `==` and `!=` operations between two
|
||||
arbitrary objects to a `bool`, but a comparison of tuples will fail if the result of comparing any
|
||||
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
```
|
||||
|
||||
@@ -35,3 +35,13 @@ def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
3 if NotBoolable() else 4
|
||||
```
|
||||
|
||||
@@ -147,3 +147,17 @@ def _(flag: bool):
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
...
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
elif NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
@@ -43,3 +43,21 @@ def _(target: int):
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
def _(target: int, flag: NotBoolable):
|
||||
y = 1
|
||||
match target:
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
case 1 if flag:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
@@ -201,14 +201,11 @@ class C:
|
||||
|
||||
c1 = C.factory("test") # okay
|
||||
|
||||
# TODO: should be `C`
|
||||
reveal_type(c1) # revealed: @Todo(return type)
|
||||
reveal_type(c1) # revealed: C
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C.get_name()) # revealed: @Todo(return type)
|
||||
reveal_type(C.get_name()) # revealed: str
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
|
||||
reveal_type(C("42").get_name()) # revealed: str
|
||||
```
|
||||
|
||||
## Descriptors only work when used as class variables
|
||||
@@ -285,6 +282,31 @@ C.descriptor = "something else"
|
||||
reveal_type(C.descriptor) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## `__get__` is called with correct arguments
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class TailoredForClassObjectAccess:
|
||||
def __get__(self, instance: None, owner: type[C]) -> int:
|
||||
return 1
|
||||
|
||||
class TailoredForInstanceAccess:
|
||||
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
|
||||
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
|
||||
|
||||
reveal_type(C.class_object_access) # revealed: int
|
||||
reveal_type(C().instance_access) # revealed: str
|
||||
|
||||
# TODO: These should emit a diagnostic
|
||||
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
|
||||
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
|
||||
```
|
||||
|
||||
## Descriptors with incorrect `__get__` signature
|
||||
|
||||
```py
|
||||
@@ -403,15 +425,15 @@ wrapper_descriptor(f, None)
|
||||
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`"
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`"
|
||||
wrapper_descriptor(1, None, type(f))
|
||||
|
||||
# Calling it with something that is not a `type` as the `owner` argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`); expected type `type`"
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type`"
|
||||
wrapper_descriptor(f, None, f)
|
||||
|
||||
# Calling it with too many positional arguments is an
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 3, got 4"
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4"
|
||||
wrapper_descriptor(f, None, type(f), "one too many")
|
||||
```
|
||||
|
||||
|
||||
@@ -182,3 +182,16 @@ class C:
|
||||
c = C()
|
||||
c("wrong") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Calls to methods
|
||||
|
||||
Tests that we also see a reference to a function if the callable is a bound method.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def square(self, x: int) -> int:
|
||||
return x * x
|
||||
|
||||
c = C()
|
||||
c.square("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
assert NotBoolable()
|
||||
```
|
||||
@@ -101,3 +101,55 @@ reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
```
|
||||
|
||||
## `__bool__` returning `NoReturn`
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
class NotBoolable:
|
||||
def __bool__(self) -> NoReturn:
|
||||
raise NotImplementedError("This object can't be converted to a boolean")
|
||||
|
||||
# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't
|
||||
# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix.
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Not callable `__bool__`
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Not-boolable union
|
||||
|
||||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__ = None if cond else 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Union with some variants implementing `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
|
||||
a = 10 if cond else NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
|
||||
if a:
|
||||
...
|
||||
```
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# PEP 695 Generics
|
||||
|
||||
## Class Declarations
|
||||
|
||||
Basic PEP 695 generics
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
data: T
|
||||
box_model_number = 695
|
||||
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
box: MyBox[int] = MyBox(5)
|
||||
|
||||
# TODO should emit a diagnostic here (str is not assignable to int)
|
||||
wrong_innards: MyBox[int] = MyBox("five")
|
||||
|
||||
# TODO reveal int, do not leak the typevar
|
||||
reveal_type(box.data) # revealed: T
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Unknown | Literal[695]
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
||||
```py
|
||||
class MyBox[T]:
|
||||
data: T
|
||||
|
||||
def __init__(self, data: T):
|
||||
self.data = data
|
||||
|
||||
# TODO not error on the subscripting
|
||||
# error: [non-subscriptable]
|
||||
class MySecureBox[T](MyBox[T]): ...
|
||||
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
# TODO reveal int
|
||||
# The @Todo(…) is misleading here. We currently treat `MyBox[T]` as a dynamic base class because we
|
||||
# don't understand generics and therefore infer `Unknown` for the `MyBox[T]` base of `MySecureBox[T]`.
|
||||
reveal_type(secure_box.data) # revealed: @Todo(instance attribute on class with dynamic base)
|
||||
```
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
This should hold true even with generics at play.
|
||||
|
||||
```pyi
|
||||
class Seq[T]: ...
|
||||
|
||||
# TODO not error on the subscripting
|
||||
class S[T](Seq[S]): ... # error: [non-subscriptable]
|
||||
|
||||
reveal_type(S) # revealed: Literal[S]
|
||||
```
|
||||
|
||||
## Type params
|
||||
|
||||
A PEP695 type variable defines a value of type `typing.TypeVar`.
|
||||
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(T) # revealed: T
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
## Minimum two constraints
|
||||
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
@@ -0,0 +1,186 @@
|
||||
# Generic classes
|
||||
|
||||
## PEP 695 syntax
|
||||
|
||||
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
|
||||
|
||||
This is a generic class defined using PEP 695 syntax:
|
||||
|
||||
```py
|
||||
class C[T]: ...
|
||||
```
|
||||
|
||||
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
|
||||
|
||||
```py
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class D[U](C[U]): ...
|
||||
```
|
||||
|
||||
A class that inherits from a generic class, but fills its type parameters with concrete types, is
|
||||
_not_ generic:
|
||||
|
||||
```py
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class E(C[int]): ...
|
||||
```
|
||||
|
||||
A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly
|
||||
uses the default value for the typevar. In this case, that default type is `Unknown`, so `F`
|
||||
inherits from `C[Unknown]` and is not itself generic.
|
||||
|
||||
```py
|
||||
class F(C): ...
|
||||
```
|
||||
|
||||
## Legacy syntax
|
||||
|
||||
This is a generic class defined using the legacy syntax:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: no error
|
||||
# error: [invalid-base]
|
||||
class C(Generic[T]): ...
|
||||
```
|
||||
|
||||
A class that inherits from a generic class, and fills its type parameters with typevars, is generic.
|
||||
|
||||
```py
|
||||
class D(C[T]): ...
|
||||
```
|
||||
|
||||
(Examples `E` and `F` from above do not have analogues in the legacy syntax.)
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
The type parameter can be specified explicitly:
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: C[int]
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(C[int]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
```py
|
||||
c: C[int] = C()
|
||||
# TODO: revealed: C[int]
|
||||
reveal_type(c) # revealed: C
|
||||
```
|
||||
|
||||
The typevars of a fully specialized generic class should no longer be visible:
|
||||
|
||||
```py
|
||||
# TODO: revealed: int
|
||||
reveal_type(c.x) # revealed: T
|
||||
```
|
||||
|
||||
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
|
||||
specific type, we infer the typevar's default type:
|
||||
|
||||
```py
|
||||
class D[T = int]: ...
|
||||
|
||||
# TODO: revealed: D[int]
|
||||
reveal_type(D()) # revealed: D
|
||||
```
|
||||
|
||||
If a typevar does not provide a default, we use `Unknown`:
|
||||
|
||||
```py
|
||||
# TODO: revealed: C[Unknown]
|
||||
reveal_type(C()) # revealed: C
|
||||
```
|
||||
|
||||
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
||||
parameter:
|
||||
|
||||
```py
|
||||
class E[T]:
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# TODO: revealed: E[int] or E[Literal[1]]
|
||||
reveal_type(E(1)) # revealed: E
|
||||
```
|
||||
|
||||
The types inferred from a type context and from a constructor parameter must be consistent with each
|
||||
other:
|
||||
|
||||
```py
|
||||
# TODO: error
|
||||
wrong_innards: E[int] = E("five")
|
||||
```
|
||||
|
||||
## Generic subclass
|
||||
|
||||
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
|
||||
propagate through:
|
||||
|
||||
```py
|
||||
class Base[T]:
|
||||
x: T
|
||||
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub[U](Base[U]): ...
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: int
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(Base[int].x) # revealed: Unknown
|
||||
# TODO: revealed: int
|
||||
reveal_type(Sub[int].x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Cyclic class definition
|
||||
|
||||
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
|
||||
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
|
||||
|
||||
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
class Base[T]: ...
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
`string_annotation.py`:
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
`bare_annotation.py`:
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
|
||||
# TODO: error: [unresolved-reference]
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
||||
@@ -0,0 +1,244 @@
|
||||
# Generic functions
|
||||
|
||||
## Typevar must be used at least twice
|
||||
|
||||
If you're only using a typevar for a single parameter, you don't need the typevar — just use
|
||||
`object` (or the typevar's upper bound):
|
||||
|
||||
```py
|
||||
# TODO: error, should be (x: object)
|
||||
def typevar_not_needed[T](x: T) -> None:
|
||||
pass
|
||||
|
||||
# TODO: error, should be (x: int)
|
||||
def bounded_typevar_not_needed[T: int](x: T) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
Typevars are only needed if you use them more than once. For instance, to specify that two
|
||||
parameters must both have the same type:
|
||||
|
||||
```py
|
||||
def two_params[T](x: T, y: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
or to specify that a return value is the same as a parameter:
|
||||
|
||||
```py
|
||||
def return_value[T](x: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
Each typevar must also appear _somewhere_ in the parameter list:
|
||||
|
||||
```py
|
||||
def absurd[T]() -> T:
|
||||
# There's no way to construct a T!
|
||||
...
|
||||
```
|
||||
|
||||
## Inferring generic function parameter types
|
||||
|
||||
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
|
||||
is bound to at each call site.
|
||||
|
||||
TODO: Note that some of the TODO revealed types have two options, since we haven't decided yet
|
||||
whether we want to infer a more specific `Literal` type where possible, or use heuristics to weaken
|
||||
the inferred type to e.g. `int`.
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> T: ...
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: int or Literal[1]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f(1)) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: float
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f(1.0)) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: bool or Literal[true]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f(True)) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: str or Literal["string"]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: T
|
||||
```
|
||||
|
||||
## Inferring “deep” generic parameter types
|
||||
|
||||
The matching up of call arguments and discovery of constraints on typevars can be a recursive
|
||||
process for arbitrarily-nested generic types in parameters.
|
||||
|
||||
```py
|
||||
def f[T](x: list[T]) -> T: ...
|
||||
|
||||
# TODO: revealed: float
|
||||
reveal_type(f([1.0, 2.0])) # revealed: T
|
||||
```
|
||||
|
||||
## Typevar constraints
|
||||
|
||||
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
|
||||
typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar
|
||||
in the function.
|
||||
|
||||
```py
|
||||
def good_param[T: int](x: T) -> None:
|
||||
# TODO: revealed: T & int
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
If the function is annotated as returning the typevar, this means that the upper bound is _not_
|
||||
assignable to that typevar, since return types are contravariant. In `bad`, we can infer that
|
||||
`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the
|
||||
return value is not guaranteed to be compatible for all `T: int`.
|
||||
|
||||
```py
|
||||
def good_return[T: int](x: T) -> T:
|
||||
return x
|
||||
|
||||
def bad_return[T: int](x: T) -> T:
|
||||
# TODO: error: int is not assignable to T
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `Literal[1]`"
|
||||
return x + 1
|
||||
```
|
||||
|
||||
## All occurrences of the same typevar have the same type
|
||||
|
||||
If a typevar appears multiple times in a function signature, all occurrences have the same type.
|
||||
|
||||
```py
|
||||
def different_types[T, S](cond: bool, t: T, s: S) -> T:
|
||||
if cond:
|
||||
return t
|
||||
else:
|
||||
# TODO: error: S is not assignable to T
|
||||
return s
|
||||
|
||||
def same_types[T](cond: bool, t1: T, t2: T) -> T:
|
||||
if cond:
|
||||
return t1
|
||||
else:
|
||||
return t2
|
||||
```
|
||||
|
||||
## All occurrences of the same constrained typevar have the same type
|
||||
|
||||
The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__`
|
||||
methods that are compatible with the return type, so the `return` expression is always well-typed:
|
||||
|
||||
```py
|
||||
def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T:
|
||||
# TODO: no error
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
This is _not_ the same as a union type, because of this additional constraint that the two
|
||||
occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types,
|
||||
and an `int` and a `str` cannot be added together:
|
||||
|
||||
```py
|
||||
def unions_are_different(t1: int | str, t2: int | str) -> int | str:
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
## Typevar inference is a unification problem
|
||||
|
||||
When inferring typevar assignments in a generic function call, we cannot simply solve constraints
|
||||
eagerly for each parameter in turn. We must solve a unification problem involving all of the
|
||||
parameters simultaneously.
|
||||
|
||||
```py
|
||||
def two_params[T](x: T, y: T) -> T:
|
||||
return x
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: str
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(two_params("a", "b")) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: str | int
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(two_params("a", 1)) # revealed: T
|
||||
```
|
||||
|
||||
```py
|
||||
def param_with_union[T](x: T | int, y: T) -> T:
|
||||
return y
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: str
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(param_with_union(1, "a")) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: str
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(param_with_union("a", "a")) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: int
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(param_with_union(1, 1)) # revealed: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: str | int
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(param_with_union("a", 1)) # revealed: T
|
||||
```
|
||||
|
||||
```py
|
||||
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
|
||||
return y
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: tuple[str, int]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[T, S]
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: tuple[str, int]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
## Inferring nested generic function calls
|
||||
|
||||
We can infer type assignments in nested calls to multiple generic functions. If they use the same
|
||||
type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below.
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> tuple[T, int]:
|
||||
return (x, 1)
|
||||
|
||||
def g[T](x: T) -> T | None:
|
||||
return x
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: tuple[str | None, int]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f(g("a"))) # revealed: tuple[T, int]
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: tuple[str, int] | None
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(g(f("a"))) # revealed: T | None
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# Legacy type variables
|
||||
|
||||
The tests in this file focus on how type variables are defined using the legacy notation. Most
|
||||
_uses_ of type variables are tested in other files in this directory; we do not duplicate every test
|
||||
for both type variable syntaxes.
|
||||
|
||||
Unless otherwise specified, all quotations come from the [Generics] section of the typing spec.
|
||||
|
||||
## Type variables
|
||||
|
||||
### Defining legacy type variables
|
||||
|
||||
> Generics can be parameterized by using a factory available in `typing` called `TypeVar`.
|
||||
|
||||
This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available
|
||||
in newer Python releases.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
```
|
||||
|
||||
### Directly assigned to a variable
|
||||
|
||||
> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as
|
||||
> part of a larger expression).
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# TODO: error
|
||||
TestList = list[TypeVar("W")]
|
||||
```
|
||||
|
||||
### `TypeVar` parameter must match variable name
|
||||
|
||||
> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# TODO: error
|
||||
T = TypeVar("Q")
|
||||
```
|
||||
|
||||
### No redefinition
|
||||
|
||||
> Type variables must not be redefined.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: error
|
||||
T = TypeVar("T")
|
||||
```
|
||||
|
||||
### Cannot have only one constraint
|
||||
|
||||
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
|
||||
> be at least two constraints, if any; specifying a single constraint is disallowed.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# TODO: error: [invalid-type-variable-constraints]
|
||||
T = TypeVar("T", int)
|
||||
```
|
||||
|
||||
[generics]: https://typing.readthedocs.io/en/latest/spec/generics.html
|
||||
@@ -0,0 +1,51 @@
|
||||
# PEP 695 Generics
|
||||
|
||||
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
|
||||
|
||||
## Type variables
|
||||
|
||||
### Defining PEP 695 type variables
|
||||
|
||||
PEP 695 introduces a new syntax for defining type variables. The resulting type variables are
|
||||
instances of `typing.TypeVar`, just like legacy type variables.
|
||||
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(type(T)) # revealed: Literal[TypeVar]
|
||||
reveal_type(T) # revealed: T
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
### Cannot have only one constraint
|
||||
|
||||
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
|
||||
> be at least two constraints, if any; specifying a single constraint is disallowed.
|
||||
|
||||
```py
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
## Invalid uses
|
||||
|
||||
Note that many of the invalid uses of legacy typevars do not apply to PEP 695 typevars, since the
|
||||
PEP 695 syntax is only allowed places where typevars are allowed.
|
||||
|
||||
## Displaying typevars
|
||||
|
||||
We use a suffix when displaying the typevars of a generic function or class. This helps distinguish
|
||||
different uses of the same typevar.
|
||||
|
||||
```py
|
||||
def f[T](x: T, y: T) -> None:
|
||||
# TODO: revealed: T@f
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
class C[T]:
|
||||
def m(self, x: T) -> None:
|
||||
# TODO: revealed: T@c
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
[pep 695]: https://peps.python.org/pep-0695/
|
||||
@@ -0,0 +1,255 @@
|
||||
# Scoping rules for type variables
|
||||
|
||||
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
|
||||
spec.
|
||||
|
||||
## Typevar used outside of generic function or class
|
||||
|
||||
Typevars may only be used in generic function or class definitions.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: error
|
||||
x: T
|
||||
|
||||
class C:
|
||||
# TODO: error
|
||||
x: T
|
||||
|
||||
def f() -> None:
|
||||
# TODO: error
|
||||
x: T
|
||||
```
|
||||
|
||||
## Legacy typevar used multiple times
|
||||
|
||||
> A type variable used in a generic function could be inferred to represent different types in the
|
||||
> same code block.
|
||||
|
||||
This only applies to typevars defined using the legacy syntax, since the PEP 695 syntax creates a
|
||||
new distinct typevar for each occurrence.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f1(x: T) -> T: ...
|
||||
def f2(x: T) -> T: ...
|
||||
|
||||
f1(1)
|
||||
f2("a")
|
||||
```
|
||||
|
||||
## Typevar inferred multiple times
|
||||
|
||||
> A type variable used in a generic function could be inferred to represent different types in the
|
||||
> same code block.
|
||||
|
||||
This also applies to a single generic function being used multiple times, instantiating the typevar
|
||||
to a different type each time.
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> T: ...
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: int or Literal[1]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f(1)) # revealed: T
|
||||
# TODO: no error
|
||||
# TODO: revealed: str or Literal["a"]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("a")) # revealed: T
|
||||
```
|
||||
|
||||
## Methods can mention class typevars
|
||||
|
||||
> A type variable used in a method of a generic class that coincides with one of the variables that
|
||||
> parameterize this class is always bound to that variable.
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def m1(self, x: T) -> T: ...
|
||||
def m2(self, x: T) -> T: ...
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: no error
|
||||
# error: [invalid-argument-type]
|
||||
c.m1(1)
|
||||
# TODO: no error
|
||||
# error: [invalid-argument-type]
|
||||
c.m2(1)
|
||||
# TODO: expected type `int`
|
||||
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `T`"
|
||||
c.m2("string")
|
||||
```
|
||||
|
||||
## Methods can mention other typevars
|
||||
|
||||
> A type variable used in a method that does not match any of the variables that parameterize the
|
||||
> class makes this method a generic function in that variable.
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
# TODO: no error
|
||||
# error: [invalid-base]
|
||||
class Legacy(Generic[T]):
|
||||
def m(self, x: T, y: S) -> S: ...
|
||||
|
||||
legacy: Legacy[int] = Legacy()
|
||||
# TODO: revealed: str
|
||||
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Invalid or unsupported `Instance` in `Type::to_type_expression`)
|
||||
```
|
||||
|
||||
With PEP 695 syntax, it is clearer that the method uses a separate typevar:
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def m[S](self, x: T, y: S) -> S: ...
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: no errors
|
||||
# TODO: revealed: str
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(c.m(1, "string")) # revealed: S
|
||||
```
|
||||
|
||||
## Unbound typevars
|
||||
|
||||
> Unbound type variables should not appear in the bodies of generic functions, or in the class
|
||||
> bodies apart from method definitions.
|
||||
|
||||
This is true with the legacy syntax:
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
def f(x: T) -> None:
|
||||
x: list[T] = []
|
||||
# TODO: error
|
||||
y: list[S] = []
|
||||
|
||||
# TODO: no error
|
||||
# error: [invalid-base]
|
||||
class C(Generic[T]):
|
||||
# TODO: error
|
||||
x: list[S] = []
|
||||
|
||||
# This is not an error, as shown in the previous test
|
||||
def m(self, x: S) -> S: ...
|
||||
```
|
||||
|
||||
This is true with PEP 695 syntax, as well, though we must use the legacy syntax to define the
|
||||
unbound typevars:
|
||||
|
||||
`pep695.py`:
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
S = TypeVar("S")
|
||||
|
||||
def f[T](x: T) -> None:
|
||||
x: list[T] = []
|
||||
# TODO: error
|
||||
y: list[S] = []
|
||||
|
||||
class C[T]:
|
||||
# TODO: error
|
||||
x: list[S] = []
|
||||
|
||||
def m1(self, x: S) -> S: ...
|
||||
def m2[S](self, x: S) -> S: ...
|
||||
```
|
||||
|
||||
## Nested formal typevars must be distinct
|
||||
|
||||
Generic functions and classes can be nested in each other, but it is an error for the same typevar
|
||||
to be used in nested generic definitions.
|
||||
|
||||
Note that the typing spec only mentions two specific versions of this rule:
|
||||
|
||||
> A generic class definition that appears inside a generic function should not use type variables
|
||||
> that parameterize the generic function.
|
||||
|
||||
and
|
||||
|
||||
> A generic class nested in another generic class cannot use the same type variables.
|
||||
|
||||
We assume that the more general form holds.
|
||||
|
||||
### Generic function within generic function
|
||||
|
||||
```py
|
||||
def f[T](x: T, y: T) -> None:
|
||||
def ok[S](a: S, b: S) -> None: ...
|
||||
|
||||
# TODO: error
|
||||
def bad[T](a: T, b: T) -> None: ...
|
||||
```
|
||||
|
||||
### Generic method within generic class
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def ok[S](self, a: S, b: S) -> None: ...
|
||||
|
||||
# TODO: error
|
||||
def bad[T](self, a: T, b: T) -> None: ...
|
||||
```
|
||||
|
||||
### Generic class within generic function
|
||||
|
||||
```py
|
||||
from typing import Iterable
|
||||
|
||||
def f[T](x: T, y: T) -> None:
|
||||
class Ok[S]: ...
|
||||
# TODO: error
|
||||
class Bad1[T]: ...
|
||||
# TODO: error
|
||||
class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
### Generic class within generic class
|
||||
|
||||
```py
|
||||
from typing import Iterable
|
||||
|
||||
class C[T]:
|
||||
class Ok1[S]: ...
|
||||
# TODO: error
|
||||
class Bad1[T]: ...
|
||||
# TODO: error
|
||||
class Bad2(Iterable[T]): ...
|
||||
```
|
||||
|
||||
## Class scopes do not cover inner scopes
|
||||
|
||||
Just like regular symbols, the typevars of a generic class are only available in that class's scope,
|
||||
and are not available in nested scopes.
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
ok1: list[T] = []
|
||||
|
||||
class Bad:
|
||||
# TODO: error
|
||||
bad: list[T] = []
|
||||
|
||||
class Inner[S]: ...
|
||||
ok2: Inner[T]
|
||||
```
|
||||
|
||||
[scoping]: https://typing.readthedocs.io/en/latest/spec/generics.html#scoping-rules-for-type-variables
|
||||
@@ -0,0 +1,131 @@
|
||||
# Case Sensitive Imports
|
||||
|
||||
```toml
|
||||
# TODO: This test should use the real file system instead of the memory file system.
|
||||
# but we can't change the file system yet because the tests would then start failing for
|
||||
# case-insensitive file systems.
|
||||
#system = "os"
|
||||
```
|
||||
|
||||
Python's import system is case-sensitive even on case-insensitive file system. This means, importing
|
||||
a module `a` should fail if the file in the search paths is named `A.py`. See
|
||||
[PEP 235](https://peps.python.org/pep-0235/).
|
||||
|
||||
## Correct casing
|
||||
|
||||
Importing a module where the name matches the file name's casing should succeed.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
```python
|
||||
from a import Foo
|
||||
|
||||
reveal_type(Foo().x) # revealed: int
|
||||
```
|
||||
|
||||
## Incorrect casing
|
||||
|
||||
Importing a module where the name does not match the file name's casing should fail.
|
||||
|
||||
`A.py`:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
```python
|
||||
# error: [unresolved-import]
|
||||
from a import Foo
|
||||
```
|
||||
|
||||
## Multiple search paths with different cased modules
|
||||
|
||||
The resolved module is the first matching the file name's casing but Python falls back to later
|
||||
search paths if the file name's casing does not match.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/search-1", "/search-2"]
|
||||
```
|
||||
|
||||
`/search-1/A.py`:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`/search-2/a.py`:
|
||||
|
||||
```py
|
||||
class Bar:
|
||||
x: str = "test"
|
||||
```
|
||||
|
||||
```python
|
||||
from A import Foo
|
||||
from a import Bar
|
||||
|
||||
reveal_type(Foo().x) # revealed: int
|
||||
reveal_type(Bar().x) # revealed: str
|
||||
```
|
||||
|
||||
## Intermediate segments
|
||||
|
||||
`db/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`db/a.py`:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`correctly_cased.py`:
|
||||
|
||||
```python
|
||||
from db.a import Foo
|
||||
|
||||
reveal_type(Foo().x) # revealed: int
|
||||
```
|
||||
|
||||
Imports where some segments are incorrectly cased should fail.
|
||||
|
||||
`incorrectly_cased.py`:
|
||||
|
||||
```python
|
||||
# error: [unresolved-import]
|
||||
from DB.a import Foo
|
||||
|
||||
# error: [unresolved-import]
|
||||
from DB.A import Foo
|
||||
|
||||
# error: [unresolved-import]
|
||||
from db.A import Foo
|
||||
```
|
||||
|
||||
## Incorrect extension casing
|
||||
|
||||
The extension of imported python modules must be `.py` or `.pyi` but not `.PY` or `Py` or any
|
||||
variant where some characters are uppercase.
|
||||
|
||||
`a.PY`:
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
```python
|
||||
# error: [unresolved-import]
|
||||
from a import Foo
|
||||
```
|
||||
@@ -26,23 +26,6 @@ from typing import TYPE_CHECKING as TC
|
||||
reveal_type(TC) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Must originate from `typing`
|
||||
|
||||
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
|
||||
with the same name:
|
||||
|
||||
`constants.py`:
|
||||
|
||||
```py
|
||||
TYPE_CHECKING: bool = False
|
||||
```
|
||||
|
||||
```py
|
||||
from constants import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: bool
|
||||
```
|
||||
|
||||
### `typing_extensions` re-export
|
||||
|
||||
This should behave in the same way as `typing.TYPE_CHECKING`:
|
||||
@@ -52,3 +35,117 @@ from typing_extensions import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## User-defined `TYPE_CHECKING`
|
||||
|
||||
If we set `TYPE_CHECKING = False` directly instead of importing it from the `typing` module, it will
|
||||
still be treated as `True` during type checking. This behavior is for compatibility with other major
|
||||
type checkers, e.g. mypy and pyright.
|
||||
|
||||
### With no type annotation
|
||||
|
||||
```py
|
||||
TYPE_CHECKING = False
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
runtime = True
|
||||
|
||||
# type_checking is treated as unconditionally assigned.
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
```
|
||||
|
||||
### With a type annotation
|
||||
|
||||
We can also define `TYPE_CHECKING` with a type annotation. The type must be one to which `bool` can
|
||||
be assigned. Even in this case, the type of `TYPE_CHECKING` is still inferred to be `Literal[True]`.
|
||||
|
||||
```py
|
||||
TYPE_CHECKING: bool = False
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
if TYPE_CHECKING:
|
||||
type_checking = True
|
||||
if not TYPE_CHECKING:
|
||||
runtime = True
|
||||
|
||||
reveal_type(type_checking) # revealed: Literal[True]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(runtime) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Importing user-defined `TYPE_CHECKING`
|
||||
|
||||
`constants.py`:
|
||||
|
||||
```py
|
||||
TYPE_CHECKING = False
|
||||
```
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
TYPE_CHECKING: bool
|
||||
# or
|
||||
TYPE_CHECKING: bool = ...
|
||||
```
|
||||
|
||||
```py
|
||||
from constants import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
|
||||
from stub import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Invalid assignment to `TYPE_CHECKING`
|
||||
|
||||
Only `False` can be assigned to `TYPE_CHECKING`; any assignment other than `False` will result in an
|
||||
error. A type annotation to which `bool` is not assignable is also an error.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING = True
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: bool = True
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: int = 1
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str = "str"
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str = False
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: Literal[False] = False
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: Literal[True] = False
|
||||
```
|
||||
|
||||
The same rules apply in a stub file:
|
||||
|
||||
```pyi
|
||||
from typing import Literal
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: str = False
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: Literal[False] = ...
|
||||
|
||||
# error: [invalid-type-checking-constant]
|
||||
TYPE_CHECKING: object = "str"
|
||||
```
|
||||
|
||||
@@ -105,7 +105,11 @@ reveal_type(x)
|
||||
|
||||
## With non-callable iterator
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def _(flag: bool):
|
||||
class NotIterable:
|
||||
if flag:
|
||||
@@ -113,7 +117,8 @@ def _(flag: bool):
|
||||
else:
|
||||
__iter__: None = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
# error: [not-iterable]
|
||||
for x in NotIterable():
|
||||
pass
|
||||
|
||||
# revealed: Unknown
|
||||
@@ -123,21 +128,25 @@ def _(flag: bool):
|
||||
|
||||
## Invalid iterable
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
||||
for x in nonsense: # error: [not-iterable]
|
||||
pass
|
||||
```
|
||||
|
||||
## New over old style iteration protocol
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
__iter__: None = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
for x in NotIterable(): # error: [not-iterable]
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -221,7 +230,11 @@ def _(flag: bool):
|
||||
|
||||
## Union type as iterable where one union element has no `__iter__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
@@ -231,14 +244,18 @@ class Test:
|
||||
return TestIter()
|
||||
|
||||
def _(flag: bool):
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
# error: [not-iterable]
|
||||
for x in Test() if flag else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has invalid `__iter__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class TestIter:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
@@ -253,7 +270,7 @@ class Test2:
|
||||
|
||||
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"
|
||||
# error: [not-iterable]
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
@@ -269,7 +286,464 @@ class Test:
|
||||
def __iter__(self) -> TestIter | int:
|
||||
return TestIter()
|
||||
|
||||
# error: [not-iterable] "Object of type `Test` is not iterable"
|
||||
# error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method"
|
||||
for x in Test():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly-not-callable `__iter__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class CustomCallable:
|
||||
if flag:
|
||||
def __call__(self, *args, **kwargs) -> Iterator:
|
||||
return Iterator()
|
||||
else:
|
||||
__call__: None = None
|
||||
|
||||
class Iterable1:
|
||||
__iter__: CustomCallable = CustomCallable()
|
||||
|
||||
class Iterable2:
|
||||
if flag:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
else:
|
||||
__iter__: None = None
|
||||
|
||||
# error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable"
|
||||
for x in Iterable1():
|
||||
# TODO... `int` might be ideal here?
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
|
||||
# error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `<bound method `__iter__` of `Iterable2`> | None`) may not be callable"
|
||||
for y in Iterable2():
|
||||
# TODO... `int` might be ideal here?
|
||||
reveal_type(y) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## `__iter__` method with a bad signature
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self, extra_arg) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## `__iter__` does not return an iterator
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Bad:
|
||||
def __iter__(self) -> int:
|
||||
return 42
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Bad():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `__iter__` returns an object with a possibly unbound `__next__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Iterator:
|
||||
if flag:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
# error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method"
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## `__iter__` returns an iterator with an invalid `__next__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Iterator1:
|
||||
def __next__(self, extra_arg) -> int:
|
||||
return 42
|
||||
|
||||
class Iterator2:
|
||||
__next__: None = None
|
||||
|
||||
class Iterable1:
|
||||
def __iter__(self) -> Iterator1:
|
||||
return Iterator1()
|
||||
|
||||
class Iterable2:
|
||||
def __iter__(self) -> Iterator2:
|
||||
return Iterator2()
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable1():
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# error: [not-iterable]
|
||||
for y in Iterable2():
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly unbound `__iter__` and bad `__getitem__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def _(flag: bool):
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
if flag:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
# invalid signature because it only accepts a `str`,
|
||||
# but the old-style iteration protocol will pass it an `int`
|
||||
def __getitem__(self, key: str) -> bytes:
|
||||
return 42
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: int | bytes
|
||||
```
|
||||
|
||||
## Possibly unbound `__iter__` and not-callable `__getitem__`
|
||||
|
||||
This snippet tests that we infer the element type correctly in the following edge case:
|
||||
|
||||
- `__iter__` is a method with the correct parameter spec that returns a valid iterator; BUT
|
||||
- `__iter__` is possibly unbound; AND
|
||||
- `__getitem__` is set to a non-callable type
|
||||
|
||||
It's important that we emit a diagnostic here, but it's also important that we still use the return
|
||||
type of the iterator's `__next__` method as the inferred type of `x` in the `for` loop:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
if flag:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
__getitem__: None = None
|
||||
|
||||
# error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable"
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound `__iter__` and possibly unbound `__getitem__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class Iterable:
|
||||
if flag1:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
if flag2:
|
||||
def __getitem__(self, key: int) -> bytes:
|
||||
return 42
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: int | bytes
|
||||
```
|
||||
|
||||
## No `__iter__` method and `__getitem__` is not callable
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Bad:
|
||||
__getitem__: None = None
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Bad():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly-not-callable `__getitem__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def _(flag: bool):
|
||||
class CustomCallable:
|
||||
if flag:
|
||||
def __call__(self, *args, **kwargs) -> int:
|
||||
return 42
|
||||
else:
|
||||
__call__: None = None
|
||||
|
||||
class Iterable1:
|
||||
__getitem__: CustomCallable = CustomCallable()
|
||||
|
||||
class Iterable2:
|
||||
if flag:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
else:
|
||||
__getitem__: None = None
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable1():
|
||||
# TODO... `int` might be ideal here?
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
|
||||
# error: [not-iterable]
|
||||
for y in Iterable2():
|
||||
# TODO... `int` might be ideal here?
|
||||
reveal_type(y) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Bad `__getitem__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Iterable:
|
||||
# invalid because it will implicitly be passed an `int`
|
||||
# by the interpreter
|
||||
def __getitem__(self, key: str) -> int:
|
||||
return 42
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound `__iter__` but definitely bound `__getitem__`
|
||||
|
||||
Here, we should not emit a diagnostic: if `__iter__` is unbound, we should fallback to
|
||||
`__getitem__`:
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> str:
|
||||
return "foo"
|
||||
|
||||
def _(flag: bool):
|
||||
class Iterable:
|
||||
if flag:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
def __getitem__(self, key: int) -> bytes:
|
||||
return b"foo"
|
||||
|
||||
for x in Iterable():
|
||||
reveal_type(x) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Possibly invalid `__iter__` methods
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
def _(flag: bool):
|
||||
class Iterable1:
|
||||
if flag:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
else:
|
||||
def __iter__(self, invalid_extra_arg) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable1():
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
class Iterable2:
|
||||
if flag:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
else:
|
||||
__iter__: None = None
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable2():
|
||||
# TODO: `int` would probably be better here:
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Possibly invalid `__next__` method
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def _(flag: bool):
|
||||
class Iterator1:
|
||||
if flag:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
else:
|
||||
def __next__(self, invalid_extra_arg) -> str:
|
||||
return "foo"
|
||||
|
||||
class Iterator2:
|
||||
if flag:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
else:
|
||||
__next__: None = None
|
||||
|
||||
class Iterable1:
|
||||
def __iter__(self) -> Iterator1:
|
||||
return Iterator1()
|
||||
|
||||
class Iterable2:
|
||||
def __iter__(self) -> Iterator2:
|
||||
return Iterator2()
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable1():
|
||||
reveal_type(x) # revealed: int | str
|
||||
|
||||
# error: [not-iterable]
|
||||
for y in Iterable2():
|
||||
# TODO: `int` would probably be better here:
|
||||
reveal_type(y) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Possibly invalid `__getitem__` methods
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def _(flag: bool):
|
||||
class Iterable1:
|
||||
if flag:
|
||||
def __getitem__(self, item: int) -> str:
|
||||
return "foo"
|
||||
else:
|
||||
__getitem__: None = None
|
||||
|
||||
class Iterable2:
|
||||
if flag:
|
||||
def __getitem__(self, item: int) -> str:
|
||||
return "foo"
|
||||
else:
|
||||
def __getitem__(self, item: str) -> int:
|
||||
return "foo"
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable1():
|
||||
# TODO: `str` might be better
|
||||
reveal_type(x) # revealed: str | Unknown
|
||||
|
||||
# error: [not-iterable]
|
||||
for y in Iterable2():
|
||||
reveal_type(y) # revealed: str | int
|
||||
```
|
||||
|
||||
## Possibly unbound `__iter__` and possibly invalid `__getitem__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def _(flag: bool, flag2: bool):
|
||||
class Iterable1:
|
||||
if flag:
|
||||
def __getitem__(self, item: int) -> str:
|
||||
return "foo"
|
||||
else:
|
||||
__getitem__: None = None
|
||||
|
||||
if flag2:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
class Iterable2:
|
||||
if flag:
|
||||
def __getitem__(self, item: int) -> str:
|
||||
return "foo"
|
||||
else:
|
||||
def __getitem__(self, item: str) -> int:
|
||||
return "foo"
|
||||
if flag2:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
# error: [not-iterable]
|
||||
for x in Iterable1():
|
||||
# TODO: `bytes | str` might be better
|
||||
reveal_type(x) # revealed: bytes | str | Unknown
|
||||
|
||||
# error: [not-iterable]
|
||||
for y in Iterable2():
|
||||
reveal_type(y) # revealed: bytes | str | int
|
||||
```
|
||||
|
||||
## Never is iterable
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
|
||||
def f(never: Never):
|
||||
for x in never:
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
@@ -116,3 +116,14 @@ def _(flag: bool, flag2: bool):
|
||||
# error: [possibly-unresolved-reference]
|
||||
y
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
while NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
@@ -97,12 +97,7 @@ else:
|
||||
## No narrowing for instances of `builtins.type`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
t = type("t", (), {})
|
||||
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
|
||||
def _(flag: bool, t: type):
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
|
||||
@@ -112,8 +112,7 @@ def _(flag: bool):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
|
||||
@@ -266,7 +266,7 @@ def _(
|
||||
if af:
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
||||
|
||||
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
|
||||
if d:
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||
|
||||
@@ -341,10 +341,12 @@ annotation are looked up lazily, even if they occur in an eager scope.
|
||||
### Eager annotations in a Python file
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
x = int
|
||||
|
||||
class C:
|
||||
var: x
|
||||
var: ClassVar[x]
|
||||
|
||||
reveal_type(C.var) # revealed: int
|
||||
|
||||
@@ -356,10 +358,12 @@ x = str
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
x = int
|
||||
|
||||
class C:
|
||||
var: x
|
||||
var: ClassVar[x]
|
||||
|
||||
reveal_type(C.var) # revealed: Unknown | str
|
||||
|
||||
@@ -369,10 +373,12 @@ x = str
|
||||
### Deferred annotations in a stub file
|
||||
|
||||
```pyi
|
||||
from typing import ClassVar
|
||||
|
||||
x = int
|
||||
|
||||
class C:
|
||||
var: x
|
||||
var: ClassVar[x]
|
||||
|
||||
reveal_type(C.var) # revealed: Unknown | str
|
||||
|
||||
|
||||
@@ -136,3 +136,42 @@ if returns_bool():
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
```
|
||||
|
||||
## Implicit global attributes in the current module override implicit globals from builtins
|
||||
|
||||
Here, we take the type of the implicit global symbol `__name__` from the `types.ModuleType` stub
|
||||
(which in this custom typeshed specifies the type as `bytes`). This is because the `main` module has
|
||||
an implicit `__name__` global that shadows the builtin `__name__` symbol.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class int: ...
|
||||
class bytes: ...
|
||||
|
||||
__name__: int = 42
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/types.pyi`:
|
||||
|
||||
```pyi
|
||||
class ModuleType:
|
||||
__name__: bytes
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
reveal_type(__name__) # revealed: bytes
|
||||
```
|
||||
|
||||
@@ -167,7 +167,7 @@ class A:
|
||||
__slots__ = ()
|
||||
__slots__ += ("a", "b")
|
||||
|
||||
reveal_type(A.__slots__) # revealed: @Todo(return type)
|
||||
reveal_type(A.__slots__) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
class B:
|
||||
__slots__ = ("c", "d")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Bad `__getitem__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Iterable:
|
||||
4 | # invalid because it will implicitly be passed an `int`
|
||||
5 | # by the interpreter
|
||||
6 | def __getitem__(self, key: str) -> int:
|
||||
7 | return 42
|
||||
8 |
|
||||
9 | # error: [not-iterable]
|
||||
10 | for x in Iterable():
|
||||
11 | reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:10:10
|
||||
|
|
||||
9 | # error: [not-iterable]
|
||||
10 | for x in Iterable():
|
||||
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
|
||||
11 | reveal_type(x) # revealed: int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:11:5
|
||||
|
|
||||
9 | # error: [not-iterable]
|
||||
10 | for x in Iterable():
|
||||
11 | reveal_type(x) # revealed: int
|
||||
| -------------- info: Revealed type is `int`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Invalid iterable
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | nonsense = 123
|
||||
2 | for x in nonsense: # error: [not-iterable]
|
||||
3 | pass
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:2:10
|
||||
|
|
||||
1 | nonsense = 123
|
||||
2 | for x in nonsense: # error: [not-iterable]
|
||||
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
|
||||
3 | pass
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - New over old style iteration protocol
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotIterable:
|
||||
2 | def __getitem__(self, key: int) -> int:
|
||||
3 | return 42
|
||||
4 | __iter__: None = None
|
||||
5 |
|
||||
6 | for x in NotIterable(): # error: [not-iterable]
|
||||
7 | pass
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:6:10
|
||||
|
|
||||
4 | __iter__: None = None
|
||||
5 |
|
||||
6 | for x in NotIterable(): # error: [not-iterable]
|
||||
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
|
||||
7 | pass
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - No `__iter__` method and `__getitem__` is not callable
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Bad:
|
||||
4 | __getitem__: None = None
|
||||
5 |
|
||||
6 | # error: [not-iterable]
|
||||
7 | for x in Bad():
|
||||
8 | reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:7:10
|
||||
|
|
||||
6 | # error: [not-iterable]
|
||||
7 | for x in Bad():
|
||||
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
|
||||
8 | reveal_type(x) # revealed: Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:8:5
|
||||
|
|
||||
6 | # error: [not-iterable]
|
||||
7 | for x in Bad():
|
||||
8 | reveal_type(x) # revealed: Unknown
|
||||
| -------------- info: Revealed type is `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly-not-callable `__getitem__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | def _(flag: bool):
|
||||
4 | class CustomCallable:
|
||||
5 | if flag:
|
||||
6 | def __call__(self, *args, **kwargs) -> int:
|
||||
7 | return 42
|
||||
8 | else:
|
||||
9 | __call__: None = None
|
||||
10 |
|
||||
11 | class Iterable1:
|
||||
12 | __getitem__: CustomCallable = CustomCallable()
|
||||
13 |
|
||||
14 | class Iterable2:
|
||||
15 | if flag:
|
||||
16 | def __getitem__(self, key: int) -> int:
|
||||
17 | return 42
|
||||
18 | else:
|
||||
19 | __getitem__: None = None
|
||||
20 |
|
||||
21 | # error: [not-iterable]
|
||||
22 | for x in Iterable1():
|
||||
23 | # TODO... `int` might be ideal here?
|
||||
24 | reveal_type(x) # revealed: int | Unknown
|
||||
25 |
|
||||
26 | # error: [not-iterable]
|
||||
27 | for y in Iterable2():
|
||||
28 | # TODO... `int` might be ideal here?
|
||||
29 | reveal_type(y) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:22:14
|
||||
|
|
||||
21 | # error: [not-iterable]
|
||||
22 | for x in Iterable1():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
|
||||
23 | # TODO... `int` might be ideal here?
|
||||
24 | reveal_type(x) # revealed: int | Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:24:9
|
||||
|
|
||||
22 | for x in Iterable1():
|
||||
23 | # TODO... `int` might be ideal here?
|
||||
24 | reveal_type(x) # revealed: int | Unknown
|
||||
| -------------- info: Revealed type is `int | Unknown`
|
||||
25 |
|
||||
26 | # error: [not-iterable]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:27:14
|
||||
|
|
||||
26 | # error: [not-iterable]
|
||||
27 | for y in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable2`> | None`) may not be callable
|
||||
28 | # TODO... `int` might be ideal here?
|
||||
29 | reveal_type(y) # revealed: int | Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | for y in Iterable2():
|
||||
28 | # TODO... `int` might be ideal here?
|
||||
29 | reveal_type(y) # revealed: int | Unknown
|
||||
| -------------- info: Revealed type is `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly invalid `__getitem__` methods
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | def _(flag: bool):
|
||||
4 | class Iterable1:
|
||||
5 | if flag:
|
||||
6 | def __getitem__(self, item: int) -> str:
|
||||
7 | return "foo"
|
||||
8 | else:
|
||||
9 | __getitem__: None = None
|
||||
10 |
|
||||
11 | class Iterable2:
|
||||
12 | if flag:
|
||||
13 | def __getitem__(self, item: int) -> str:
|
||||
14 | return "foo"
|
||||
15 | else:
|
||||
16 | def __getitem__(self, item: str) -> int:
|
||||
17 | return "foo"
|
||||
18 |
|
||||
19 | # error: [not-iterable]
|
||||
20 | for x in Iterable1():
|
||||
21 | # TODO: `str` might be better
|
||||
22 | reveal_type(x) # revealed: str | Unknown
|
||||
23 |
|
||||
24 | # error: [not-iterable]
|
||||
25 | for y in Iterable2():
|
||||
26 | reveal_type(y) # revealed: str | int
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:20:14
|
||||
|
|
||||
19 | # error: [not-iterable]
|
||||
20 | for x in Iterable1():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable1`> | None`) may not be callable
|
||||
21 | # TODO: `str` might be better
|
||||
22 | reveal_type(x) # revealed: str | Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:22:9
|
||||
|
|
||||
20 | for x in Iterable1():
|
||||
21 | # TODO: `str` might be better
|
||||
22 | reveal_type(x) # revealed: str | Unknown
|
||||
| -------------- info: Revealed type is `str | Unknown`
|
||||
23 |
|
||||
24 | # error: [not-iterable]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:25:14
|
||||
|
|
||||
24 | # error: [not-iterable]
|
||||
25 | for y in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
|
||||
26 | reveal_type(y) # revealed: str | int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:26:9
|
||||
|
|
||||
24 | # error: [not-iterable]
|
||||
25 | for y in Iterable2():
|
||||
26 | reveal_type(y) # revealed: str | int
|
||||
| -------------- info: Revealed type is `str | int`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly invalid `__iter__` methods
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Iterator:
|
||||
4 | def __next__(self) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | def _(flag: bool):
|
||||
8 | class Iterable1:
|
||||
9 | if flag:
|
||||
10 | def __iter__(self) -> Iterator:
|
||||
11 | return Iterator()
|
||||
12 | else:
|
||||
13 | def __iter__(self, invalid_extra_arg) -> Iterator:
|
||||
14 | return Iterator()
|
||||
15 |
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable1():
|
||||
18 | reveal_type(x) # revealed: int
|
||||
19 |
|
||||
20 | class Iterable2:
|
||||
21 | if flag:
|
||||
22 | def __iter__(self) -> Iterator:
|
||||
23 | return Iterator()
|
||||
24 | else:
|
||||
25 | __iter__: None = None
|
||||
26 |
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable2():
|
||||
29 | # TODO: `int` would probably be better here:
|
||||
30 | reveal_type(x) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:17:14
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable1():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `<bound method `__iter__` of `Iterable1`> | <bound method `__iter__` of `Iterable1`>`) may have an invalid signature (expected `def __iter__(self): ...`)
|
||||
18 | reveal_type(x) # revealed: int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:18:9
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable1():
|
||||
18 | reveal_type(x) # revealed: int
|
||||
| -------------- info: Revealed type is `int`
|
||||
19 |
|
||||
20 | class Iterable2:
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:28:14
|
||||
|
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `<bound method `__iter__` of `Iterable2`> | None`) may not be callable
|
||||
29 | # TODO: `int` would probably be better here:
|
||||
30 | reveal_type(x) # revealed: int | Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:30:9
|
||||
|
|
||||
28 | for x in Iterable2():
|
||||
29 | # TODO: `int` would probably be better here:
|
||||
30 | reveal_type(x) # revealed: int | Unknown
|
||||
| -------------- info: Revealed type is `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly invalid `__next__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | def _(flag: bool):
|
||||
4 | class Iterator1:
|
||||
5 | if flag:
|
||||
6 | def __next__(self) -> int:
|
||||
7 | return 42
|
||||
8 | else:
|
||||
9 | def __next__(self, invalid_extra_arg) -> str:
|
||||
10 | return "foo"
|
||||
11 |
|
||||
12 | class Iterator2:
|
||||
13 | if flag:
|
||||
14 | def __next__(self) -> int:
|
||||
15 | return 42
|
||||
16 | else:
|
||||
17 | __next__: None = None
|
||||
18 |
|
||||
19 | class Iterable1:
|
||||
20 | def __iter__(self) -> Iterator1:
|
||||
21 | return Iterator1()
|
||||
22 |
|
||||
23 | class Iterable2:
|
||||
24 | def __iter__(self) -> Iterator2:
|
||||
25 | return Iterator2()
|
||||
26 |
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable1():
|
||||
29 | reveal_type(x) # revealed: int | str
|
||||
30 |
|
||||
31 | # error: [not-iterable]
|
||||
32 | for y in Iterable2():
|
||||
33 | # TODO: `int` would probably be better here:
|
||||
34 | reveal_type(y) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:28:14
|
||||
|
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable1():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
|
||||
29 | reveal_type(x) # revealed: int | str
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable1():
|
||||
29 | reveal_type(x) # revealed: int | str
|
||||
| -------------- info: Revealed type is `int | str`
|
||||
30 |
|
||||
31 | # error: [not-iterable]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:32:14
|
||||
|
|
||||
31 | # error: [not-iterable]
|
||||
32 | for y in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
|
||||
33 | # TODO: `int` would probably be better here:
|
||||
34 | reveal_type(y) # revealed: int | Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:34:9
|
||||
|
|
||||
32 | for y in Iterable2():
|
||||
33 | # TODO: `int` would probably be better here:
|
||||
34 | reveal_type(y) # revealed: int | Unknown
|
||||
| -------------- info: Revealed type is `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | def _(flag: bool):
|
||||
4 | class Iterator:
|
||||
5 | def __next__(self) -> int:
|
||||
6 | return 42
|
||||
7 |
|
||||
8 | class Iterable:
|
||||
9 | if flag:
|
||||
10 | def __iter__(self) -> Iterator:
|
||||
11 | return Iterator()
|
||||
12 | # invalid signature because it only accepts a `str`,
|
||||
13 | # but the old-style iteration protocol will pass it an `int`
|
||||
14 | def __getitem__(self, key: str) -> bytes:
|
||||
15 | return 42
|
||||
16 |
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Iterable():
|
||||
19 | reveal_type(x) # revealed: int | bytes
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:18:14
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Iterable():
|
||||
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
|
||||
19 | reveal_type(x) # revealed: int | bytes
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:19:9
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Iterable():
|
||||
19 | reveal_type(x) # revealed: int | bytes
|
||||
| -------------- info: Revealed type is `int | bytes`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Iterator:
|
||||
4 | def __next__(self) -> bytes:
|
||||
5 | return b"foo"
|
||||
6 |
|
||||
7 | def _(flag: bool, flag2: bool):
|
||||
8 | class Iterable1:
|
||||
9 | if flag:
|
||||
10 | def __getitem__(self, item: int) -> str:
|
||||
11 | return "foo"
|
||||
12 | else:
|
||||
13 | __getitem__: None = None
|
||||
14 |
|
||||
15 | if flag2:
|
||||
16 | def __iter__(self) -> Iterator:
|
||||
17 | return Iterator()
|
||||
18 |
|
||||
19 | class Iterable2:
|
||||
20 | if flag:
|
||||
21 | def __getitem__(self, item: int) -> str:
|
||||
22 | return "foo"
|
||||
23 | else:
|
||||
24 | def __getitem__(self, item: str) -> int:
|
||||
25 | return "foo"
|
||||
26 | if flag2:
|
||||
27 | def __iter__(self) -> Iterator:
|
||||
28 | return Iterator()
|
||||
29 |
|
||||
30 | # error: [not-iterable]
|
||||
31 | for x in Iterable1():
|
||||
32 | # TODO: `bytes | str` might be better
|
||||
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
||||
34 |
|
||||
35 | # error: [not-iterable]
|
||||
36 | for y in Iterable2():
|
||||
37 | reveal_type(y) # revealed: bytes | str | int
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:31:14
|
||||
|
|
||||
30 | # error: [not-iterable]
|
||||
31 | for x in Iterable1():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable1`> | None`) may not be callable
|
||||
32 | # TODO: `bytes | str` might be better
|
||||
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:33:9
|
||||
|
|
||||
31 | for x in Iterable1():
|
||||
32 | # TODO: `bytes | str` might be better
|
||||
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
||||
| -------------- info: Revealed type is `bytes | str | Unknown`
|
||||
34 |
|
||||
35 | # error: [not-iterable]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:36:14
|
||||
|
|
||||
35 | # error: [not-iterable]
|
||||
36 | for y in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`)
|
||||
may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
|
||||
37 | reveal_type(y) # revealed: bytes | str | int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:37:9
|
||||
|
|
||||
35 | # error: [not-iterable]
|
||||
36 | for y in Iterable2():
|
||||
37 | reveal_type(y) # revealed: bytes | str | int
|
||||
| -------------- info: Revealed type is `bytes | str | int`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Iterator:
|
||||
4 | def __next__(self) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | def _(flag1: bool, flag2: bool):
|
||||
8 | class Iterable:
|
||||
9 | if flag1:
|
||||
10 | def __iter__(self) -> Iterator:
|
||||
11 | return Iterator()
|
||||
12 | if flag2:
|
||||
13 | def __getitem__(self, key: int) -> bytes:
|
||||
14 | return 42
|
||||
15 |
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable():
|
||||
18 | reveal_type(x) # revealed: int | bytes
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:17:14
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable():
|
||||
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
|
||||
18 | reveal_type(x) # revealed: int | bytes
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:18:9
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable():
|
||||
18 | reveal_type(x) # revealed: int | bytes
|
||||
| -------------- info: Revealed type is `int | bytes`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Union type as iterable where one union element has invalid `__iter__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class TestIter:
|
||||
4 | def __next__(self) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | class Test:
|
||||
8 | def __iter__(self) -> TestIter:
|
||||
9 | return TestIter()
|
||||
10 |
|
||||
11 | class Test2:
|
||||
12 | def __iter__(self) -> int:
|
||||
13 | return 42
|
||||
14 |
|
||||
15 | def _(flag: bool):
|
||||
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Test() if flag else Test2():
|
||||
19 | reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:18:14
|
||||
|
|
||||
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Test() if flag else Test2():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
|
||||
19 | reveal_type(x) # revealed: int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:19:9
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Test() if flag else Test2():
|
||||
19 | reveal_type(x) # revealed: int
|
||||
| -------------- info: Revealed type is `int`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - Union type as iterable where one union element has no `__iter__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class TestIter:
|
||||
4 | def __next__(self) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | class Test:
|
||||
8 | def __iter__(self) -> TestIter:
|
||||
9 | return TestIter()
|
||||
10 |
|
||||
11 | def _(flag: bool):
|
||||
12 | # error: [not-iterable]
|
||||
13 | for x in Test() if flag else 42:
|
||||
14 | reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:13:14
|
||||
|
|
||||
11 | def _(flag: bool):
|
||||
12 | # error: [not-iterable]
|
||||
13 | for x in Test() if flag else 42:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
|
||||
14 | reveal_type(x) # revealed: int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:14:9
|
||||
|
|
||||
12 | # error: [not-iterable]
|
||||
13 | for x in Test() if flag else 42:
|
||||
14 | reveal_type(x) # revealed: int
|
||||
| -------------- info: Revealed type is `int`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - With non-callable iterator
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | def _(flag: bool):
|
||||
4 | class NotIterable:
|
||||
5 | if flag:
|
||||
6 | __iter__: int = 1
|
||||
7 | else:
|
||||
8 | __iter__: None = None
|
||||
9 |
|
||||
10 | # error: [not-iterable]
|
||||
11 | for x in NotIterable():
|
||||
12 | pass
|
||||
13 |
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:11:14
|
||||
|
|
||||
10 | # error: [not-iterable]
|
||||
11 | for x in NotIterable():
|
||||
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
|
||||
12 | pass
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
warning: lint:possibly-unresolved-reference
|
||||
--> /src/mdtest_snippet.py:16:17
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:16:5
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| -------------- info: Revealed type is `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - `__iter__` does not return an iterator
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Bad:
|
||||
4 | def __iter__(self) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | # error: [not-iterable]
|
||||
8 | for x in Bad():
|
||||
9 | reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:8:10
|
||||
|
|
||||
7 | # error: [not-iterable]
|
||||
8 | for x in Bad():
|
||||
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
|
||||
9 | reveal_type(x) # revealed: Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:9:5
|
||||
|
|
||||
7 | # error: [not-iterable]
|
||||
8 | for x in Bad():
|
||||
9 | reveal_type(x) # revealed: Unknown
|
||||
| -------------- info: Revealed type is `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - `__iter__` method with a bad signature
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Iterator:
|
||||
4 | def __next__(self) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | class Iterable:
|
||||
8 | def __iter__(self, extra_arg) -> Iterator:
|
||||
9 | return Iterator()
|
||||
10 |
|
||||
11 | # error: [not-iterable]
|
||||
12 | for x in Iterable():
|
||||
13 | reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:12:10
|
||||
|
|
||||
11 | # error: [not-iterable]
|
||||
12 | for x in Iterable():
|
||||
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
|
||||
13 | reveal_type(x) # revealed: int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:13:5
|
||||
|
|
||||
11 | # error: [not-iterable]
|
||||
12 | for x in Iterable():
|
||||
13 | reveal_type(x) # revealed: int
|
||||
| -------------- info: Revealed type is `int`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: for.md - For loops - `__iter__` returns an iterator with an invalid `__next__` method
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing_extensions import reveal_type
|
||||
2 |
|
||||
3 | class Iterator1:
|
||||
4 | def __next__(self, extra_arg) -> int:
|
||||
5 | return 42
|
||||
6 |
|
||||
7 | class Iterator2:
|
||||
8 | __next__: None = None
|
||||
9 |
|
||||
10 | class Iterable1:
|
||||
11 | def __iter__(self) -> Iterator1:
|
||||
12 | return Iterator1()
|
||||
13 |
|
||||
14 | class Iterable2:
|
||||
15 | def __iter__(self) -> Iterator2:
|
||||
16 | return Iterator2()
|
||||
17 |
|
||||
18 | # error: [not-iterable]
|
||||
19 | for x in Iterable1():
|
||||
20 | reveal_type(x) # revealed: int
|
||||
21 |
|
||||
22 | # error: [not-iterable]
|
||||
23 | for y in Iterable2():
|
||||
24 | reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:19:10
|
||||
|
|
||||
18 | # error: [not-iterable]
|
||||
19 | for x in Iterable1():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
|
||||
20 | reveal_type(x) # revealed: int
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:20:5
|
||||
|
|
||||
18 | # error: [not-iterable]
|
||||
19 | for x in Iterable1():
|
||||
20 | reveal_type(x) # revealed: int
|
||||
| -------------- info: Revealed type is `int`
|
||||
21 |
|
||||
22 | # error: [not-iterable]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:23:10
|
||||
|
|
||||
22 | # error: [not-iterable]
|
||||
23 | for y in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
|
||||
24 | reveal_type(y) # revealed: Unknown
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
--> /src/mdtest_snippet.py:24:5
|
||||
|
|
||||
22 | # error: [not-iterable]
|
||||
23 | for y in Iterable2():
|
||||
24 | reveal_type(y) # revealed: Unknown
|
||||
| -------------- info: Revealed type is `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | a = NotBoolable()
|
||||
5 |
|
||||
6 | # error: [unsupported-bool-conversion]
|
||||
7 | 10 and a and True
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:7:8
|
||||
|
|
||||
6 | # error: [unsupported-bool-conversion]
|
||||
7 | 10 and a and True
|
||||
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Calls to methods
|
||||
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 square(self, x: int) -> int:
|
||||
3 | return x * x
|
||||
4 |
|
||||
5 | c = C()
|
||||
6 | c.square("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
--> /src/mdtest_snippet.py:6:10
|
||||
|
|
||||
5 | c = C()
|
||||
6 | c.square("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`x`) of bound method `square`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:2:22
|
||||
|
|
||||
1 | class C:
|
||||
2 | def square(self, x: int) -> int:
|
||||
| ------ info: parameter declared in function definition here
|
||||
3 | return x * x
|
||||
|
|
||||
|
||||
```
|
||||
@@ -28,7 +28,7 @@ error: lint:invalid-argument-type
|
||||
|
|
||||
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`
|
||||
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`
|
||||
|
|
||||
::: /src/mdtest_snippet.py:2:24
|
||||
|
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | class WithContains:
|
||||
5 | def __contains__(self, item) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | 10 in WithContains()
|
||||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | 10 in WithContains()
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:11:1
|
||||
|
|
||||
9 | 10 in WithContains()
|
||||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | # error: [unsupported-bool-conversion]
|
||||
5 | not NotBoolable()
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:5:1
|
||||
|
|
||||
4 | # error: [unsupported-bool-conversion]
|
||||
5 | not NotBoolable()
|
||||
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
3 |
|
||||
4 | class Comparable:
|
||||
5 | def __lt__(self, item) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
7 |
|
||||
8 | def __gt__(self, item) -> NotBoolable:
|
||||
9 | return NotBoolable()
|
||||
10 |
|
||||
11 | # error: [unsupported-bool-conversion]
|
||||
12 | 10 < Comparable() < 20
|
||||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
15 |
|
||||
16 | Comparable() < Comparable() # fine
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:12:1
|
||||
|
|
||||
11 | # error: [unsupported-bool-conversion]
|
||||
12 | 10 < Comparable() < 20
|
||||
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:14:1
|
||||
|
|
||||
12 | 10 < Comparable() < 20
|
||||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
15 |
|
||||
16 | Comparable() < Comparable() # fine
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 5
|
||||
3 |
|
||||
4 | class Comparable:
|
||||
5 | def __lt__(self, other) -> NotBoolable:
|
||||
6 | return NotBoolable()
|
||||
7 |
|
||||
8 | def __gt__(self, other) -> NotBoolable:
|
||||
9 | return NotBoolable()
|
||||
10 |
|
||||
11 | a = (1, Comparable())
|
||||
12 | b = (1, Comparable())
|
||||
13 |
|
||||
14 | # error: [unsupported-bool-conversion]
|
||||
15 | a < b < b
|
||||
16 |
|
||||
17 | a < b # fine
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:15:1
|
||||
|
|
||||
14 | # error: [unsupported-bool-conversion]
|
||||
15 | a < b < b
|
||||
| ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
|
||||
16 |
|
||||
17 | a < b # fine
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | def __eq__(self, other) -> NotBoolable:
|
||||
3 | return NotBoolable()
|
||||
4 |
|
||||
5 | class NotBoolable:
|
||||
6 | __bool__ = None
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
| ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -22,7 +22,7 @@ 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
|
||||
| ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -15,3 +15,30 @@ class Bar(Foo[Bar]): ...
|
||||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Access to attributes declarated in stubs
|
||||
|
||||
Unlike regular Python modules, stub files often omit the right-hand side in declarations, including
|
||||
in class scope. However, from the perspective of the type checker, we have to treat them as bindings
|
||||
too. That is, `symbol: type` is the same as `symbol: type = ...`.
|
||||
|
||||
One implication of this is that we'll always treat symbols in class scope as safe to be accessed
|
||||
from the class object itself. We'll never infer a "pure instance attribute" from a stub.
|
||||
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
class_or_instance_var: int
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
from b import C
|
||||
|
||||
# No error here, since we treat `class_or_instance_var` as bound on the class.
|
||||
reveal_type(C.class_or_instance_var) # revealed: int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Declarations in stubs
|
||||
|
||||
Unlike regular Python modules, stub files often declare module-global variables without initializing
|
||||
them. If these symbols are then used in the same stub, applying regular logic would lead to an
|
||||
undefined variable access error.
|
||||
|
||||
However, from the perspective of the type checker, we should treat something like `symbol: type` the
|
||||
same as `symbol: type = ...`. In other words, assume these are bindings too.
|
||||
|
||||
```pyi
|
||||
from typing import Literal
|
||||
|
||||
CONSTANT: Literal[42]
|
||||
|
||||
# No error here, even though the variable is not initialized.
|
||||
uses_constant: int = CONSTANT
|
||||
```
|
||||
@@ -25,7 +25,7 @@ reveal_type(y) # revealed: Unknown
|
||||
def _(n: int):
|
||||
a = b"abcde"[n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -44,10 +44,10 @@ b[::0] # error: [zero-stepsize-in-slice]
|
||||
def _(m: int, n: int):
|
||||
byte_slice1 = b[m:n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
def _(s: bytes) -> bytes:
|
||||
byte_slice2 = s[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
@@ -12,13 +12,13 @@ x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo(return type)
|
||||
reveal_type(x[0]) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type)
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type)
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Assignments within list assignment
|
||||
|
||||
@@ -22,7 +22,7 @@ reveal_type(b) # revealed: Unknown
|
||||
def _(n: int):
|
||||
a = "abcde"[n]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -76,11 +76,11 @@ def _(m: int, n: int, s2: str):
|
||||
|
||||
substring1 = s[m:n]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
reveal_type(substring1) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
substring2 = s2[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
reveal_type(substring2) # revealed: @Todo(return type of decorated function)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user