Compare commits
32 Commits
bump-allo
...
charlie/un
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d579767f78 | ||
|
|
25aabec814 | ||
|
|
0e71485ea9 | ||
|
|
43a9d282f7 | ||
|
|
6f357b8b45 | ||
|
|
73d9f11a9c | ||
|
|
d6c6db5a44 | ||
|
|
56d985a972 | ||
|
|
b3e0655cc9 | ||
|
|
06baffec9e | ||
|
|
67a2ae800a | ||
|
|
7a2c75e2fc | ||
|
|
9ee44637ca | ||
|
|
733341ab39 | ||
|
|
341a25eec1 | ||
|
|
38e178e914 | ||
|
|
daccb3f4f3 | ||
|
|
c858afe03a | ||
|
|
3c1c3199d0 | ||
|
|
fbfe2cb2f5 | ||
|
|
1c311e4fdb | ||
|
|
12177a42e3 | ||
|
|
dfb08856eb | ||
|
|
94d817e1a5 | ||
|
|
9296bd4e3f | ||
|
|
da824ba316 | ||
|
|
012198a1b0 | ||
|
|
fbab04fbe1 | ||
|
|
9aa43d5f91 | ||
|
|
966563c79b | ||
|
|
27edadec29 | ||
|
|
2e2b1b460f |
@@ -10,7 +10,7 @@ exclude: |
|
||||
crates/ruff_python_formatter/tests/snapshots/.*|
|
||||
crates/ruff_python_resolver/resources/.*|
|
||||
crates/ruff_python_resolver/tests/snapshots/.*|
|
||||
crates/red_knot/resources/.*
|
||||
crates/red_knot_workspace/resources/.*
|
||||
)$
|
||||
|
||||
repos:
|
||||
@@ -43,7 +43,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.23.5
|
||||
rev: v1.23.6
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -57,7 +57,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.5
|
||||
rev: v0.5.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,5 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.6
|
||||
|
||||
Ruff 0.5.6 automatically enables linting and formatting of notebooks in *preview mode*.
|
||||
You can opt-out of this behavior by adding `*.ipynb` to the `extend-exclude` setting.
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
### Preview features
|
||||
|
||||
- Enable notebooks by default in preview mode ([#12621](https://github.com/astral-sh/ruff/pull/12621))
|
||||
- \[`flake8-builtins`\] Implement import, lambda, and module shadowing ([#12546](https://github.com/astral-sh/ruff/pull/12546))
|
||||
- \[`pydoclint`\] Add `docstring-missing-returns` (`DOC201`) and `docstring-extraneous-returns` (`DOC202`) ([#12485](https://github.com/astral-sh/ruff/pull/12485))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-return`\] Exempt cached properties and other property-like decorators from explicit return rule (`RET501`) ([#12563](https://github.com/astral-sh/ruff/pull/12563))
|
||||
|
||||
### Server
|
||||
|
||||
- Make server panic hook more error resilient ([#12610](https://github.com/astral-sh/ruff/pull/12610))
|
||||
- Use `$/logTrace` for server trace logs in Zed and VS Code ([#12564](https://github.com/astral-sh/ruff/pull/12564))
|
||||
- Keep track of deleted cells for reorder change request ([#12575](https://github.com/astral-sh/ruff/pull/12575))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-implicit-str-concat`\] Always allow explicit multi-line concatenations when implicit concatenations are banned ([#12532](https://github.com/astral-sh/ruff/pull/12532))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-async`\] Avoid flagging `asyncio.timeout`s as unused when the context manager includes `asyncio.TaskGroup` ([#12605](https://github.com/astral-sh/ruff/pull/12605))
|
||||
- \[`flake8-slots`\] Avoid recommending `__slots__` for classes that inherit from more than `namedtuple` ([#12531](https://github.com/astral-sh/ruff/pull/12531))
|
||||
- \[`isort`\] Avoid marking required imports as unused ([#12537](https://github.com/astral-sh/ruff/pull/12537))
|
||||
- \[`isort`\] Preserve trailing inline comments on import-from statements ([#12498](https://github.com/astral-sh/ruff/pull/12498))
|
||||
- \[`pycodestyle`\] Add newlines before comments (`E305`) ([#12606](https://github.com/astral-sh/ruff/pull/12606))
|
||||
- \[`pycodestyle`\] Don't attach comments with mismatched indents ([#12604](https://github.com/astral-sh/ruff/pull/12604))
|
||||
- \[`pyflakes`\] Fix preview-mode bugs in `F401` when attempting to autofix unused first-party submodule imports in an `__init__.py` file ([#12569](https://github.com/astral-sh/ruff/pull/12569))
|
||||
- \[`pylint`\] Respect start index in `unnecessary-list-index-lookup` ([#12603](https://github.com/astral-sh/ruff/pull/12603))
|
||||
- \[`pyupgrade`\] Avoid recommending no-argument super in `slots=True` dataclasses ([#12530](https://github.com/astral-sh/ruff/pull/12530))
|
||||
- \[`pyupgrade`\] Use colon rather than dot formatting for integer-only types ([#12534](https://github.com/astral-sh/ruff/pull/12534))
|
||||
- Fix NFKC normalization bug when removing unused imports ([#12571](https://github.com/astral-sh/ruff/pull/12571))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Consider more stdlib decorators to be property-like ([#12583](https://github.com/astral-sh/ruff/pull/12583))
|
||||
- Improve handling of metaclasses in various linter rules ([#12579](https://github.com/astral-sh/ruff/pull/12579))
|
||||
- Improve consistency between linter rules in determining whether a function is property ([#12581](https://github.com/astral-sh/ruff/pull/12581))
|
||||
|
||||
## 0.5.5
|
||||
|
||||
### Preview features
|
||||
|
||||
85
Cargo.lock
generated
85
Cargo.lock
generated
@@ -326,9 +326,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.11"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
|
||||
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -336,9 +336,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.11"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
|
||||
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -379,9 +379,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.11"
|
||||
version = "4.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -1053,9 +1053,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@@ -1530,9 +1530,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5a8e22be64dfa1123429350872e7be33594dbf5ae5212c90c5890e71966d1d"
|
||||
checksum = "8c81974681ab4f0cc9fe49cad56f821d1cc67a08cd2caa9b5d58b0adaa5dd36d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
@@ -1945,13 +1945,10 @@ dependencies = [
|
||||
"crossbeam",
|
||||
"ctrlc",
|
||||
"filetime",
|
||||
"notify",
|
||||
"rayon",
|
||||
"red_knot_module_resolver",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
@@ -1999,6 +1996,23 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_workspace"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
"notify",
|
||||
"red_knot_module_resolver",
|
||||
"red_knot_python_semantic",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -2021,9 +2035,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.5"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2080,7 +2094,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2137,7 +2151,7 @@ dependencies = [
|
||||
"codspeed-criterion-compat",
|
||||
"mimalloc",
|
||||
"once_cell",
|
||||
"red_knot",
|
||||
"red_knot_workspace",
|
||||
"ruff_db",
|
||||
"ruff_linter",
|
||||
"ruff_python_ast",
|
||||
@@ -2266,7 +2280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2586,7 +2600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -2726,7 +2740,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?rev=0cae5c52a3240172ef0be5c9d19e63448c53397c#0cae5c52a3240172ef0be5c9d19e63448c53397c"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?rev=635e23943c095077c4a423488ac829b4ae0bfa77#635e23943c095077c4a423488ac829b4ae0bfa77"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"boomphf",
|
||||
@@ -2746,12 +2760,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?rev=0cae5c52a3240172ef0be5c9d19e63448c53397c#0cae5c52a3240172ef0be5c9d19e63448c53397c"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?rev=635e23943c095077c4a423488ac829b4ae0bfa77#635e23943c095077c4a423488ac829b4ae0bfa77"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?rev=0cae5c52a3240172ef0be5c9d19e63448c53397c#0cae5c52a3240172ef0be5c9d19e63448c53397c"
|
||||
source = "git+https://github.com/MichaReiser/salsa.git?rev=635e23943c095077c4a423488ac829b4ae0bfa77#635e23943c095077c4a423488ac829b4ae0bfa77"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -2855,9 +2869,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.121"
|
||||
version = "1.0.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
|
||||
checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3038,12 +3052,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -3181,9 +3196,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.16"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -3193,18 +3208,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.17"
|
||||
version = "0.22.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -3838,9 +3853,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.6"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -35,9 +35,9 @@ ruff_source_file = { path = "crates/ruff_source_file" }
|
||||
ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot = { path = "crates/red_knot" }
|
||||
red_knot_module_resolver = { path = "crates/red_knot_module_resolver" }
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_workspace = { path = "crates/red_knot_workspace" }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
@@ -107,7 +107,7 @@ rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/MichaReiser/salsa.git", rev = "0cae5c52a3240172ef0be5c9d19e63448c53397c" }
|
||||
salsa = { git = "https://github.com/MichaReiser/salsa.git", rev = "635e23943c095077c4a423488ac829b4ae0bfa77" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
11
README.md
11
README.md
@@ -35,7 +35,7 @@ An extremely fast Python linter and code formatter, written in Rust.
|
||||
- 📏 Over [800 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
|
||||
of popular Flake8 plugins, like flake8-bugbear
|
||||
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/integrations/) for
|
||||
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
|
||||
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup)
|
||||
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#pyprojecttoml-discovery)
|
||||
|
||||
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
|
||||
@@ -136,8 +136,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.5.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.5.6/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.6/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,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.5.5
|
||||
rev: v0.5.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -179,8 +179,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
- id: ruff-format
|
||||
```
|
||||
|
||||
Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or
|
||||
alongside any other editor through the [Ruff LSP](https://github.com/astral-sh/ruff-lsp).
|
||||
Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or with [various other editors](https://docs.astral.sh/ruff/editors/setup).
|
||||
|
||||
Ruff can also be used as a [GitHub Action](https://github.com/features/actions) via
|
||||
[`ruff-action`](https://github.com/chartboost/ruff-action):
|
||||
|
||||
@@ -10,12 +10,12 @@ doc-valid-idents = [
|
||||
"SCREAMING_SNAKE_CASE",
|
||||
"SQLAlchemy",
|
||||
"StackOverflow",
|
||||
"PyCharm",
|
||||
]
|
||||
|
||||
ignore-interior-mutability = [
|
||||
# Interned is read-only. The wrapped `Rc` never gets updated.
|
||||
"ruff_formatter::format_element::Interned",
|
||||
|
||||
# The expression is read-only.
|
||||
# The expression is read-only.
|
||||
"ruff_python_ast::hashable::HashableExpr",
|
||||
]
|
||||
|
||||
@@ -13,26 +13,23 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_module_resolver = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
notify = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-tree = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
filetime = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ use tracing_subscriber::layer::{Context, Filter, SubscriberExt};
|
||||
use tracing_subscriber::{Layer, Registry};
|
||||
use tracing_tree::time::Uptime;
|
||||
|
||||
use red_knot::db::RootDatabase;
|
||||
use red_knot::watch;
|
||||
use red_knot::watch::WorkspaceWatcher;
|
||||
use red_knot::workspace::WorkspaceMetadata;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings};
|
||||
use ruff_db::system::{OsSystem, System, SystemPathBuf};
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn main() -> anyhow::Result<()> {
|
||||
extra_paths,
|
||||
workspace_root: workspace_metadata.root().to_path_buf(),
|
||||
custom_typeshed: custom_typeshed_dir,
|
||||
site_packages: None,
|
||||
site_packages: vec![],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
use crate::db::RootDatabase;
|
||||
use crate::watch::Watcher;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use tracing::info;
|
||||
|
||||
/// Wrapper around a [`Watcher`] that watches the relevant paths of a workspace.
|
||||
pub struct WorkspaceWatcher {
|
||||
watcher: Watcher,
|
||||
|
||||
/// The paths that need to be watched. This includes paths for which setting up file watching failed.
|
||||
watched_paths: FxHashSet<SystemPathBuf>,
|
||||
|
||||
/// Paths that should be watched but setting up the watcher failed for some reason.
|
||||
/// This should be rare.
|
||||
errored_paths: Vec<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl WorkspaceWatcher {
|
||||
/// Create a new workspace watcher.
|
||||
pub fn new(watcher: Watcher, db: &RootDatabase) -> Self {
|
||||
let mut watcher = Self {
|
||||
watcher,
|
||||
watched_paths: FxHashSet::default(),
|
||||
errored_paths: Vec::new(),
|
||||
};
|
||||
|
||||
watcher.update(db);
|
||||
|
||||
watcher
|
||||
}
|
||||
|
||||
pub fn update(&mut self, db: &RootDatabase) {
|
||||
let new_watch_paths = db.workspace().paths_to_watch(db);
|
||||
|
||||
let mut added_folders = new_watch_paths.difference(&self.watched_paths).peekable();
|
||||
let mut removed_folders = self.watched_paths.difference(&new_watch_paths).peekable();
|
||||
|
||||
if added_folders.peek().is_none() && removed_folders.peek().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for added_folder in added_folders {
|
||||
// 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(added_folder) {
|
||||
// TODO: Log a user-facing warning.
|
||||
tracing::warn!("Failed to setup watcher for path '{added_folder}': {error}. You have to restart Ruff after making changes to files under this path or you might see stale results.");
|
||||
self.errored_paths.push(added_folder.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for removed_path in removed_folders {
|
||||
if let Some(index) = self
|
||||
.errored_paths
|
||||
.iter()
|
||||
.position(|path| path == removed_path)
|
||||
{
|
||||
self.errored_paths.swap_remove(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(error) = self.watcher.unwatch(removed_path) {
|
||||
info!("Failed to remove the file watcher for the path '{removed_path}: {error}.");
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Set up file watchers for {}",
|
||||
DisplayWatchedPaths {
|
||||
paths: &new_watch_paths
|
||||
}
|
||||
);
|
||||
|
||||
self.watched_paths = new_watch_paths;
|
||||
}
|
||||
|
||||
/// Returns `true` if setting up watching for any path failed.
|
||||
pub fn has_errored_paths(&self) -> bool {
|
||||
!self.errored_paths.is_empty()
|
||||
}
|
||||
|
||||
pub fn flush(&self) {
|
||||
self.watcher.flush();
|
||||
}
|
||||
|
||||
pub fn stop(self) {
|
||||
self.watcher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayWatchedPaths<'a> {
|
||||
paths: &'a FxHashSet<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisplayWatchedPaths<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_char('[')?;
|
||||
|
||||
let mut iter = self.paths.iter();
|
||||
if let Some(first) = iter.next() {
|
||||
write!(f, "\"{first}\"")?;
|
||||
|
||||
for path in iter {
|
||||
write!(f, ", \"{path}\"")?;
|
||||
}
|
||||
}
|
||||
|
||||
f.write_char(']')
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use filetime::FileTime;
|
||||
use salsa::Setter;
|
||||
|
||||
use red_knot::db::RootDatabase;
|
||||
use red_knot::watch;
|
||||
use red_knot::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot::workspace::WorkspaceMetadata;
|
||||
use red_knot_module_resolver::{resolve_module, ModuleName};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::program::{Program, ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::source::source_text;
|
||||
@@ -21,7 +21,10 @@ struct TestCase {
|
||||
db: RootDatabase,
|
||||
watcher: Option<WorkspaceWatcher>,
|
||||
changes_receiver: crossbeam::channel::Receiver<Vec<watch::ChangeEvent>>,
|
||||
temp_dir: tempfile::TempDir,
|
||||
/// The temporary directory that contains the test files.
|
||||
/// We need to hold on to it in the test case or the temp files get deleted.
|
||||
_temp_dir: tempfile::TempDir,
|
||||
root_dir: SystemPathBuf,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
@@ -30,7 +33,7 @@ impl TestCase {
|
||||
}
|
||||
|
||||
fn root_path(&self) -> &SystemPath {
|
||||
SystemPath::from_std_path(self.temp_dir.path()).unwrap()
|
||||
&self.root_dir
|
||||
}
|
||||
|
||||
fn db(&self) -> &RootDatabase {
|
||||
@@ -42,19 +45,63 @@ impl TestCase {
|
||||
}
|
||||
|
||||
fn stop_watch(&mut self) -> Vec<watch::ChangeEvent> {
|
||||
if let Some(watcher) = self.watcher.take() {
|
||||
// Give the watcher some time to catch up.
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
self.try_stop_watch(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none.")
|
||||
}
|
||||
|
||||
fn try_stop_watch(&mut self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
let watcher = self
|
||||
.watcher
|
||||
.take()
|
||||
.expect("Cannot call `stop_watch` more than once.");
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
watcher.flush();
|
||||
watcher.stop();
|
||||
|
||||
for event in &self.changes_receiver {
|
||||
all_events.extend(event);
|
||||
}
|
||||
|
||||
if all_events.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_take_watch_changes(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none.")
|
||||
}
|
||||
|
||||
fn try_take_watch_changes(&self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
let Some(watcher) = &self.watcher else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut all_events = self
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
watcher.flush();
|
||||
|
||||
while let Ok(event) = self
|
||||
.changes_receiver
|
||||
.recv_timeout(Duration::from_millis(10))
|
||||
{
|
||||
all_events.extend(event);
|
||||
watcher.flush();
|
||||
watcher.stop();
|
||||
}
|
||||
|
||||
let mut all_events = Vec::new();
|
||||
for events in &self.changes_receiver {
|
||||
all_events.extend(events);
|
||||
if all_events.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
all_events
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
fn update_search_path_settings(
|
||||
@@ -88,28 +135,62 @@ impl TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
fn setup<I, P>(workspace_files: I) -> anyhow::Result<TestCase>
|
||||
trait SetupFiles {
|
||||
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
|
||||
where
|
||||
I: IntoIterator<Item = (P, &'static str)>,
|
||||
P: AsRef<SystemPath>,
|
||||
{
|
||||
setup_with_search_paths(workspace_files, |_root, workspace_path| {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: None,
|
||||
fn setup(self, _root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()> {
|
||||
for (relative_path, content) in self {
|
||||
let relative_path = relative_path.as_ref();
|
||||
let absolute_path = workspace_path.join(relative_path);
|
||||
if let Some(parent) = absolute_path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("Failed to create parent directory for file '{relative_path}'.",)
|
||||
})?;
|
||||
}
|
||||
|
||||
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()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> SetupFiles for F
|
||||
where
|
||||
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
|
||||
{
|
||||
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()> {
|
||||
self(root_path, workspace_path)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
{
|
||||
setup_with_search_paths(setup_files, |_root, workspace_path| SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn setup_with_search_paths<I, P>(
|
||||
workspace_files: I,
|
||||
fn setup_with_search_paths<F>(
|
||||
setup_files: F,
|
||||
create_search_paths: impl FnOnce(&SystemPath, &SystemPath) -> SearchPathSettings,
|
||||
) -> anyhow::Result<TestCase>
|
||||
where
|
||||
I: IntoIterator<Item = (P, &'static str)>,
|
||||
P: AsRef<SystemPath>,
|
||||
F: SetupFiles,
|
||||
{
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
||||
@@ -132,18 +213,9 @@ where
|
||||
std::fs::create_dir_all(workspace_path.as_std_path())
|
||||
.with_context(|| format!("Failed to create workspace directory '{workspace_path}'",))?;
|
||||
|
||||
for (relative_path, content) in workspace_files {
|
||||
let relative_path = relative_path.as_ref();
|
||||
let absolute_path = workspace_path.join(relative_path);
|
||||
if let Some(parent) = absolute_path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("Failed to create parent directory for file '{relative_path}'.",)
|
||||
})?;
|
||||
}
|
||||
|
||||
std::fs::write(absolute_path.as_std_path(), content)
|
||||
.with_context(|| format!("Failed to write file '{relative_path}'"))?;
|
||||
}
|
||||
setup_files
|
||||
.setup(&root_path, &workspace_path)
|
||||
.context("Failed to setup test files")?;
|
||||
|
||||
let system = OsSystem::new(&workspace_path);
|
||||
|
||||
@@ -178,30 +250,43 @@ where
|
||||
db,
|
||||
changes_receiver: receiver,
|
||||
watcher: Some(watcher),
|
||||
temp_dir,
|
||||
_temp_dir: temp_dir,
|
||||
root_dir: root_path,
|
||||
};
|
||||
|
||||
// Sometimes the file watcher reports changes for events that happened before the watcher was started.
|
||||
// Do a best effort at dropping these events.
|
||||
test_case.try_take_watch_changes(Duration::from_millis(100));
|
||||
|
||||
Ok(test_case)
|
||||
}
|
||||
|
||||
/// The precision of the last modified time is platform dependent and not arbitrarily precise.
|
||||
/// This method sleeps until the last modified time of a newly created file changes. This guarantees
|
||||
/// that the last modified time of any file written **after** this method completes should be different.
|
||||
fn next_io_tick() {
|
||||
let temp = tempfile::tempfile().unwrap();
|
||||
/// Updates the content of a file and ensures that the last modified file time is updated.
|
||||
fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()> {
|
||||
let path = path.as_ref().as_std_path();
|
||||
|
||||
let last_modified = FileTime::from_last_modification_time(&temp.metadata().unwrap());
|
||||
let metadata = path.metadata()?;
|
||||
let last_modified_time = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(false)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
loop {
|
||||
filetime::set_file_handle_times(&temp, None, Some(FileTime::now())).unwrap();
|
||||
file.sync_all()?;
|
||||
|
||||
let new_last_modified = FileTime::from_last_modification_time(&temp.metadata().unwrap());
|
||||
let modified_time = filetime::FileTime::from_last_modification_time(&path.metadata()?);
|
||||
|
||||
if new_last_modified != last_modified {
|
||||
break;
|
||||
if modified_time != last_modified_time {
|
||||
break Ok(());
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_nanos(100));
|
||||
std::thread::sleep(Duration::from_nanos(10));
|
||||
|
||||
filetime::set_file_handle_times(&file, None, Some(filetime::FileTime::now()))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,8 +345,7 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), foo_source);
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
|
||||
|
||||
next_io_tick();
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 2')")?;
|
||||
update_file(&foo_path, "print('Version 2')")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
@@ -275,49 +359,6 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn changed_metadata() -> anyhow::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut case = setup([("foo.py", "")])?;
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
next_io_tick();
|
||||
std::fs::set_permissions(
|
||||
foo_path.as_std_path(),
|
||||
std::fs::Permissions::from_mode(0o777),
|
||||
)
|
||||
.with_context(|| "Failed to set file permissions.")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_file() -> anyhow::Result<()> {
|
||||
let foo_source = "print('Hello, world!')";
|
||||
@@ -495,7 +536,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
])?;
|
||||
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some(),);
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_some());
|
||||
|
||||
let sub_path = case.workspace_path("sub");
|
||||
let init_file = case
|
||||
@@ -656,7 +697,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(root_path.join("site_packages")),
|
||||
site_packages: vec![root_path.join("site_packages")],
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -693,7 +734,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
// Register site-packages as a search path.
|
||||
case.update_search_path_settings(|settings| SearchPathSettings {
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
..settings.clone()
|
||||
});
|
||||
|
||||
@@ -716,22 +757,478 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(root_path.join("site_packages")),
|
||||
site_packages: vec![root_path.join("site_packages")],
|
||||
}
|
||||
})?;
|
||||
|
||||
// Remove site packages from the search path settings.
|
||||
let site_packages = case.root_path().join("site_packages");
|
||||
case.update_search_path_settings(|settings| SearchPathSettings {
|
||||
site_packages: None,
|
||||
site_packages: vec![],
|
||||
..settings.clone()
|
||||
});
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
let changes = case.try_stop_watch(Duration::from_millis(100));
|
||||
|
||||
assert_eq!(changes, &[]);
|
||||
assert_eq!(changes, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch a workspace that contains two files where one file is a hardlink to another.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - workspace
|
||||
/// |- foo.py
|
||||
/// |- bar.py (hard link to foo.py)
|
||||
/// ```
|
||||
///
|
||||
/// # Linux
|
||||
/// `inotify` only emits a single change event for the file that was changed.
|
||||
/// Other files that point to the same inode (hardlinks) won't get updated.
|
||||
///
|
||||
/// For reference: VS Code and PyCharm have the same behavior where the results for one of the
|
||||
/// files are stale.
|
||||
///
|
||||
/// # Windows
|
||||
/// I haven't found any documentation that states the notification behavior on Windows but what
|
||||
/// we're seeing is that Windows only emits a single event, similar to Linux.
|
||||
#[test]
|
||||
fn hard_links_in_workspace() -> anyhow::Result<()> {
|
||||
let mut case = setup(|_root: &SystemPath, workspace: &SystemPath| {
|
||||
let foo_path = workspace.join("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
// Create a hardlink to `foo`
|
||||
let bar_path = workspace.join("bar.py");
|
||||
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
|
||||
.context("Failed to create hard link from foo.py -> bar.py")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
let foo = case.system_file(&foo_path).unwrap();
|
||||
let bar_path = case.workspace_path("bar.py");
|
||||
let bar = case.system_file(&bar_path).unwrap();
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
|
||||
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
|
||||
// macOS is the only platform that emits events for every hardlink.
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch a workspace that contains one file that is a hardlink to a file outside the workspace.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - foo.py
|
||||
/// - workspace
|
||||
/// |- bar.py (hard link to /foo.py)
|
||||
/// ```
|
||||
///
|
||||
/// # Linux
|
||||
/// inotiyf doesn't support observing changes to hard linked files.
|
||||
///
|
||||
/// > Note: when monitoring a directory, events are not generated for
|
||||
/// > the files inside the directory when the events are performed via
|
||||
/// > a pathname (i.e., a link) that lies outside the monitored
|
||||
/// > directory. [source](https://man7.org/linux/man-pages/man7/inotify.7.html)
|
||||
///
|
||||
/// # Windows
|
||||
/// > Retrieves information that describes the changes within the specified directory.
|
||||
///
|
||||
/// [source](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw)
|
||||
///
|
||||
/// My interpretation of this is that Windows doesn't support observing changes made to
|
||||
/// hard linked files outside the workspace.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "linux",
|
||||
ignore = "inotify doesn't support observing changes to hard linked files."
|
||||
)]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "windows doesn't support observing changes to hard linked files."
|
||||
)]
|
||||
fn hard_links_to_target_outside_workspace() -> anyhow::Result<()> {
|
||||
let mut case = setup(|root: &SystemPath, workspace: &SystemPath| {
|
||||
let foo_path = root.join("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
// Create a hardlink to `foo`
|
||||
let bar_path = workspace.join("bar.py");
|
||||
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
|
||||
.context("Failed to create hard link from foo.py -> bar.py")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let foo_path = case.root_path().join("foo.py");
|
||||
let foo = case.system_file(&foo_path).unwrap();
|
||||
let bar_path = case.workspace_path("bar.py");
|
||||
let bar = case.system_file(&bar_path).unwrap();
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
|
||||
|
||||
// Write to the hard link target.
|
||||
update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix {
|
||||
//! Tests that make use of unix specific file-system features.
|
||||
use super::*;
|
||||
|
||||
/// Changes the metadata of the only file in the workspace.
|
||||
#[test]
|
||||
fn changed_metadata() -> anyhow::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut case = setup([("foo.py", "")])?;
|
||||
let foo_path = case.workspace_path("foo.py");
|
||||
|
||||
let foo = case.system_file(&foo_path)?;
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
std::fs::set_permissions(
|
||||
foo_path.as_std_path(),
|
||||
std::fs::Permissions::from_mode(0o777),
|
||||
)
|
||||
.with_context(|| "Failed to set file permissions.")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
Some(
|
||||
std::fs::metadata(foo_path.as_std_path())
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A workspace path is a symlink to a file outside the workspace.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - bar
|
||||
/// |- baz.py
|
||||
///
|
||||
/// - workspace
|
||||
/// |- bar -> /bar
|
||||
/// ```
|
||||
///
|
||||
/// # macOS
|
||||
/// This test case isn't supported on macOS.
|
||||
/// macOS uses `FSEvents` and `FSEvents` doesn't emit an event if a file in a symlinked directory is changed.
|
||||
///
|
||||
/// > Generally speaking, when working with file system event notifications, you will probably want to use lstat,
|
||||
/// > because changes to the underlying file will not result in a change notification for the directory containing
|
||||
/// > the symbolic link to that file. However, if you are working with a controlled file structure in
|
||||
/// > which symbolic links always point within your watched tree, you might have reason to use stat.
|
||||
///
|
||||
/// [source](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html#//apple_ref/doc/uid/TP40005289-CH4-SW4)
|
||||
///
|
||||
/// Pyright also does not support this case.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "macos",
|
||||
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(|root: &SystemPath, workspace: &SystemPath| {
|
||||
// Set up the symlink target.
|
||||
let link_target = root.join("bar");
|
||||
std::fs::create_dir_all(link_target.as_std_path())
|
||||
.context("Failed to create link target directory")?;
|
||||
let baz_original = link_target.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.context("Failed to write link target file")?;
|
||||
|
||||
// Create a symlink inside the workspace
|
||||
let bar = workspace.join("bar");
|
||||
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
|
||||
.context("Failed to create symlink to bar package")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_workspace = case.workspace_path("bar/baz.py");
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(
|
||||
baz.file().path(case.db()).as_system_path(),
|
||||
Some(&*baz_workspace)
|
||||
);
|
||||
|
||||
let baz_original = case.root_path().join("bar/baz.py");
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(baz_original, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.take_watch_changes();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
// Write to the symlink source.
|
||||
update_file(baz_workspace, "def baz(): print('Version 3')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): print('Version 3')"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Workspace contains a symlink to another directory inside the workspace.
|
||||
/// Changes to files in the symlinked directory should be reflected
|
||||
/// to all files.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - workspace
|
||||
/// | - bar -> /workspace/patched/bar
|
||||
/// |
|
||||
/// | - patched
|
||||
/// | |-- bar
|
||||
/// | | |- baz.py
|
||||
/// |
|
||||
/// |-- foo.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlink_inside_workspace() -> anyhow::Result<()> {
|
||||
let mut case = setup(|_root: &SystemPath, workspace: &SystemPath| {
|
||||
// Set up the symlink target.
|
||||
let link_target = workspace.join("patched/bar");
|
||||
std::fs::create_dir_all(link_target.as_std_path())
|
||||
.context("Failed to create link target directory")?;
|
||||
let baz_original = link_target.join("baz.py");
|
||||
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
|
||||
.context("Failed to write link target file")?;
|
||||
|
||||
// Create a symlink inside site-packages
|
||||
let bar_in_workspace = workspace.join("bar");
|
||||
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_workspace.as_std_path())
|
||||
.context("Failed to create symlink to bar package")?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let bar_baz = case.workspace_path("bar/baz.py");
|
||||
|
||||
let patched_bar_baz = case.workspace_path("patched/bar/baz.py");
|
||||
let patched_bar_baz_file = case.system_file(&patched_bar_baz).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), patched_bar_baz_file).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(baz.file().path(case.db()).as_system_path(), Some(&*bar_baz));
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(&patched_bar_baz, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
// The file watcher is guaranteed to emit one event for the changed file, but it isn't specified
|
||||
// if the event is emitted for the "original" or linked path because both paths are watched.
|
||||
// The best we can assert here is that one of the files should have been updated.
|
||||
//
|
||||
// In a perfect world, the file watcher would emit two events, one for the original file and
|
||||
// one for the symlink. I tried parcel/watcher, node's `fs.watch` and `chokidar` and
|
||||
// only `chokidar seems to support it (used by Pyright).
|
||||
//
|
||||
// I further tested how good editor support is for symlinked files and it is not good ;)
|
||||
// * VS Code doesn't update the file content if a file gets changed through a symlink
|
||||
// * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot).
|
||||
//
|
||||
// That's why I think it's fine to not support this case for now.
|
||||
|
||||
let patched_baz_text = source_text(case.db(), patched_bar_baz_file);
|
||||
let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')";
|
||||
|
||||
let bar_baz_text = source_text(case.db(), baz.file());
|
||||
let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')";
|
||||
|
||||
assert!(
|
||||
did_update_patched_baz || did_update_bar_baz,
|
||||
"Expected one of the files to be updated but neither file was updated.\nOriginal: {patched_baz_text}\nSymlinked: {bar_baz_text}",
|
||||
patched_baz_text = patched_baz_text.as_str(),
|
||||
bar_baz_text = bar_baz_text.as_str()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A module search path is a symlink.
|
||||
///
|
||||
/// Setup:
|
||||
/// ```text
|
||||
/// - site-packages
|
||||
/// | - bar/baz.py
|
||||
///
|
||||
/// - workspace
|
||||
/// |-- .venv/lib/python3.12/site-packages -> /site-packages
|
||||
/// |
|
||||
/// |-- foo.py
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlinked_module_search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_search_paths(
|
||||
|root: &SystemPath, workspace: &SystemPath| {
|
||||
// Set up the symlink target.
|
||||
let site_packages = root.join("site-packages");
|
||||
let bar = site_packages.join("bar");
|
||||
std::fs::create_dir_all(bar.as_std_path())
|
||||
.context("Failed to create bar directory")?;
|
||||
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 = workspace.join(".venv/lib/python3.12/site-packages");
|
||||
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
|
||||
.context("Failed to create .venv directory")?;
|
||||
std::os::unix::fs::symlink(
|
||||
site_packages.as_std_path(),
|
||||
venv_site_packages.as_std_path(),
|
||||
)
|
||||
.context("Failed to create symlink to site-packages")?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|_root, workspace| SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![workspace.join(".venv/lib/python3.12/site-packages")],
|
||||
},
|
||||
)?;
|
||||
|
||||
let baz = resolve_module(
|
||||
case.db().upcast(),
|
||||
ModuleName::new_static("bar.baz").unwrap(),
|
||||
)
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_site_packages_path =
|
||||
case.workspace_path(".venv/lib/python3.12/site-packages/bar/baz.py");
|
||||
let baz_site_packages = case.system_file(&baz_site_packages_path).unwrap();
|
||||
let baz_original = case.root_path().join("site-packages/bar/baz.py");
|
||||
let baz_original_file = case.system_file(&baz_original).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_site_packages).as_str(),
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(
|
||||
baz.file().path(case.db()).as_system_path(),
|
||||
Some(&*baz_original)
|
||||
);
|
||||
|
||||
// Write to the symlink target.
|
||||
update_file(&baz_original, "def baz(): print('Version 2')")
|
||||
.context("Failed to update bar/baz.py")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
// It would be nice if this is supported but the underlying file system watchers
|
||||
// only emit a single event. For reference
|
||||
// * VS Code doesn't update the file content if a file gets changed through a symlink
|
||||
// * PyCharm doesn't update diagnostics if a symlinked module is changed (same as red knot).
|
||||
// We could add support for it by keeping a reverse map from `real_path` to symlinked path but
|
||||
// it doesn't seem worth doing considering that as prominent tools like PyCharm don't support it.
|
||||
// Pyright does support it, thanks to chokidar.
|
||||
assert_ne!(
|
||||
source_text(case.db(), baz_site_packages).as_str(),
|
||||
"def baz(): print('Version 2')"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,22 +382,27 @@ enum SearchPathInner {
|
||||
pub(crate) struct SearchPath(Arc<SearchPathInner>);
|
||||
|
||||
impl SearchPath {
|
||||
fn directory_path(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<SystemPathBuf> {
|
||||
let canonicalized = system.canonicalize_path(&root).unwrap_or(root);
|
||||
if system.is_directory(&canonicalized) {
|
||||
Ok(canonicalized)
|
||||
} else {
|
||||
Err(SearchPathValidationError::NotADirectory(canonicalized))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new "Extra" search path
|
||||
pub(crate) fn extra(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
|
||||
if system.is_directory(&root) {
|
||||
Ok(Self(Arc::new(SearchPathInner::Extra(root))))
|
||||
} else {
|
||||
Err(SearchPathValidationError::NotADirectory(root))
|
||||
}
|
||||
Ok(Self(Arc::new(SearchPathInner::Extra(
|
||||
Self::directory_path(system, root)?,
|
||||
))))
|
||||
}
|
||||
|
||||
/// Create a new first-party search path, pointing to the user code we were directly invoked on
|
||||
pub(crate) fn first_party(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
|
||||
if system.is_directory(&root) {
|
||||
Ok(Self(Arc::new(SearchPathInner::FirstParty(root))))
|
||||
} else {
|
||||
Err(SearchPathValidationError::NotADirectory(root))
|
||||
}
|
||||
Ok(Self(Arc::new(SearchPathInner::FirstParty(
|
||||
Self::directory_path(system, root)?,
|
||||
))))
|
||||
}
|
||||
|
||||
/// Create a new standard-library search path pointing to a custom directory on disk
|
||||
@@ -408,12 +413,13 @@ impl SearchPath {
|
||||
typeshed.to_path_buf(),
|
||||
));
|
||||
}
|
||||
let stdlib = typeshed.join("stdlib");
|
||||
if !system.is_directory(&stdlib) {
|
||||
return Err(SearchPathValidationError::NoStdlibSubdirectory(
|
||||
typeshed.to_path_buf(),
|
||||
));
|
||||
}
|
||||
let stdlib =
|
||||
Self::directory_path(system, typeshed.join("stdlib")).map_err(|err| match err {
|
||||
SearchPathValidationError::NotADirectory(path) => {
|
||||
SearchPathValidationError::NoStdlibSubdirectory(path)
|
||||
}
|
||||
err => err,
|
||||
})?;
|
||||
let typeshed_versions =
|
||||
system_path_to_file(db.upcast(), stdlib.join("VERSIONS")).map_err(|err| match err {
|
||||
FileError::NotFound => SearchPathValidationError::NoVersionsFile(typeshed),
|
||||
@@ -444,20 +450,16 @@ impl SearchPath {
|
||||
system: &dyn System,
|
||||
root: SystemPathBuf,
|
||||
) -> SearchPathResult<Self> {
|
||||
if system.is_directory(&root) {
|
||||
Ok(Self(Arc::new(SearchPathInner::SitePackages(root))))
|
||||
} else {
|
||||
Err(SearchPathValidationError::NotADirectory(root))
|
||||
}
|
||||
Ok(Self(Arc::new(SearchPathInner::SitePackages(
|
||||
Self::directory_path(system, root)?,
|
||||
))))
|
||||
}
|
||||
|
||||
/// Create a new search path pointing to an editable installation
|
||||
pub(crate) fn editable(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
|
||||
if system.is_directory(&root) {
|
||||
Ok(Self(Arc::new(SearchPathInner::Editable(root))))
|
||||
} else {
|
||||
Err(SearchPathValidationError::NotADirectory(root))
|
||||
}
|
||||
Ok(Self(Arc::new(SearchPathInner::Editable(
|
||||
Self::directory_path(system, root)?,
|
||||
))))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -477,12 +479,6 @@ impl SearchPath {
|
||||
)
|
||||
}
|
||||
|
||||
/// Does this search path point to the `site-packages` directory?
|
||||
#[must_use]
|
||||
pub(crate) fn is_site_packages(&self) -> bool {
|
||||
matches!(&*self.0, SearchPathInner::SitePackages(_))
|
||||
}
|
||||
|
||||
fn is_valid_extension(&self, extension: &str) -> bool {
|
||||
if self.is_standard_library() {
|
||||
extension == "pyi"
|
||||
|
||||
@@ -2,12 +2,11 @@ use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module::{Module, ModuleKind};
|
||||
@@ -161,12 +160,6 @@ fn try_resolve_module_resolution_settings(
|
||||
SearchPath::vendored_stdlib()
|
||||
});
|
||||
|
||||
if let Some(site_packages) = site_packages {
|
||||
files.try_add_root(db.upcast(), site_packages, FileRootKind::LibrarySearchPath);
|
||||
|
||||
static_search_paths.push(SearchPath::site_packages(system, site_packages.clone())?);
|
||||
};
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
|
||||
|
||||
let target_version = program.target_version(db.upcast());
|
||||
@@ -192,6 +185,7 @@ fn try_resolve_module_resolution_settings(
|
||||
Ok(ModuleResolutionSettings {
|
||||
target_version,
|
||||
static_search_paths,
|
||||
site_packages_paths: site_packages.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,52 +195,27 @@ pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSetting
|
||||
try_resolve_module_resolution_settings(db).unwrap()
|
||||
}
|
||||
|
||||
/// Collect all dynamic search paths:
|
||||
/// search paths listed in `.pth` files in the `site-packages` directory
|
||||
/// due to editable installations of third-party packages.
|
||||
/// Collect all dynamic search paths. For each `site-packages` path:
|
||||
/// - Collect that `site-packages` path
|
||||
/// - Collect any search paths listed in `.pth` files in that `site-packages` directory
|
||||
/// due to editable installations of third-party packages.
|
||||
///
|
||||
/// The editable-install search paths for the first `site-packages` directory
|
||||
/// should come between the two `site-packages` directories when it comes to
|
||||
/// module-resolution priority.
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn editable_install_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
let settings = module_resolution_settings(db);
|
||||
let static_search_paths = &settings.static_search_paths;
|
||||
pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
let ModuleResolutionSettings {
|
||||
target_version: _,
|
||||
static_search_paths,
|
||||
site_packages_paths,
|
||||
} = module_resolution_settings(db);
|
||||
|
||||
let site_packages = static_search_paths
|
||||
.iter()
|
||||
.find(|path| path.is_site_packages());
|
||||
let mut dynamic_paths = Vec::new();
|
||||
|
||||
let Some(site_packages) = site_packages else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let site_packages = site_packages
|
||||
.as_system_path()
|
||||
.expect("Expected site-packages never to be a VendoredPath!");
|
||||
|
||||
let mut dynamic_paths = Vec::default();
|
||||
|
||||
// This query needs to be re-executed each time a `.pth` file
|
||||
// is added, modified or removed from the `site-packages` directory.
|
||||
// However, we don't use Salsa queries to read the source text of `.pth` files;
|
||||
// we use the APIs on the `System` trait directly. As such, add a dependency on the
|
||||
// site-package directory's revision.
|
||||
if let Some(site_packages_root) = db.files().root(db.upcast(), site_packages) {
|
||||
let _ = site_packages_root.revision(db.upcast());
|
||||
}
|
||||
|
||||
// As well as modules installed directly into `site-packages`,
|
||||
// the directory may also contain `.pth` files.
|
||||
// Each `.pth` file in `site-packages` may contain one or more lines
|
||||
// containing a (relative or absolute) path.
|
||||
// Each of these paths may point to an editable install of a package,
|
||||
// so should be considered an additional search path.
|
||||
let Ok(pth_file_iterator) = PthFileIterator::new(db, site_packages) else {
|
||||
if site_packages_paths.is_empty() {
|
||||
return dynamic_paths;
|
||||
};
|
||||
|
||||
// The Python documentation specifies that `.pth` files in `site-packages`
|
||||
// are processed in alphabetical order, so collecting and then sorting is necessary.
|
||||
// https://docs.python.org/3/library/site.html#module-site
|
||||
let mut all_pth_files: Vec<PthFile> = pth_file_iterator.collect();
|
||||
all_pth_files.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
}
|
||||
|
||||
let mut existing_paths: FxHashSet<_> = static_search_paths
|
||||
.iter()
|
||||
@@ -254,14 +223,51 @@ pub(crate) fn editable_install_resolution_paths(db: &dyn Db) -> Vec<SearchPath>
|
||||
.map(Cow::Borrowed)
|
||||
.collect();
|
||||
|
||||
dynamic_paths.reserve(all_pth_files.len());
|
||||
let files = db.files();
|
||||
let system = db.system();
|
||||
|
||||
for pth_file in &all_pth_files {
|
||||
for installation in pth_file.editable_installations() {
|
||||
if existing_paths.insert(Cow::Owned(
|
||||
installation.as_system_path().unwrap().to_path_buf(),
|
||||
)) {
|
||||
dynamic_paths.push(installation);
|
||||
for site_packages_dir in site_packages_paths {
|
||||
if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) {
|
||||
continue;
|
||||
}
|
||||
let site_packages_root = files.try_add_root(
|
||||
db.upcast(),
|
||||
site_packages_dir,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
// This query needs to be re-executed each time a `.pth` file
|
||||
// is added, modified or removed from the `site-packages` directory.
|
||||
// However, we don't use Salsa queries to read the source text of `.pth` files;
|
||||
// we use the APIs on the `System` trait directly. As such, add a dependency on the
|
||||
// site-package directory's revision.
|
||||
site_packages_root.revision(db.upcast());
|
||||
|
||||
dynamic_paths
|
||||
.push(SearchPath::site_packages(system, site_packages_dir.to_owned()).unwrap());
|
||||
|
||||
// As well as modules installed directly into `site-packages`,
|
||||
// the directory may also contain `.pth` files.
|
||||
// Each `.pth` file in `site-packages` may contain one or more lines
|
||||
// containing a (relative or absolute) path.
|
||||
// Each of these paths may point to an editable install of a package,
|
||||
// so should be considered an additional search path.
|
||||
let Ok(pth_file_iterator) = PthFileIterator::new(db, site_packages_dir) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// The Python documentation specifies that `.pth` files in `site-packages`
|
||||
// are processed in alphabetical order, so collecting and then sorting is necessary.
|
||||
// https://docs.python.org/3/library/site.html#module-site
|
||||
let mut all_pth_files: Vec<PthFile> = pth_file_iterator.collect();
|
||||
all_pth_files.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
|
||||
for pth_file in &all_pth_files {
|
||||
for installation in pth_file.editable_installations() {
|
||||
if existing_paths.insert(Cow::Owned(
|
||||
installation.as_system_path().unwrap().to_path_buf(),
|
||||
)) {
|
||||
dynamic_paths.push(installation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +300,7 @@ impl<'db> Iterator for SearchPathIterator<'db> {
|
||||
|
||||
static_paths.next().or_else(|| {
|
||||
dynamic_paths
|
||||
.get_or_insert_with(|| editable_install_resolution_paths(*db).iter())
|
||||
.get_or_insert_with(|| dynamic_resolution_paths(*db).iter())
|
||||
.next()
|
||||
})
|
||||
}
|
||||
@@ -404,9 +410,18 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ModuleResolutionSettings {
|
||||
target_version: TargetVersion,
|
||||
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
/// These shouldn't ever change unless the config settings themselves change.
|
||||
static_search_paths: Vec<SearchPath>,
|
||||
|
||||
/// site-packages paths are not included in the above field:
|
||||
/// if there are multiple site-packages paths, editable installations can appear
|
||||
/// *between* the site-packages paths on `sys.path` at runtime.
|
||||
/// That means we can't know where a second or third `site-packages` path should sit
|
||||
/// in terms of module-resolution priority until we've discovered the editable installs
|
||||
/// for the first `site-packages` path
|
||||
site_packages_paths: Vec<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl ModuleResolutionSettings {
|
||||
@@ -625,7 +640,7 @@ impl PackageKind {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::{system_path_to_file, File, FilePath};
|
||||
use ruff_db::system::{DbWithTestSystem, OsSystem, SystemPath};
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::testing::{
|
||||
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
|
||||
};
|
||||
@@ -1153,7 +1168,9 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn symlink() -> anyhow::Result<()> {
|
||||
use crate::db::tests::TestDb;
|
||||
use ruff_db::program::Program;
|
||||
use ruff_db::system::{OsSystem, SystemPath};
|
||||
|
||||
let mut db = TestDb::new();
|
||||
|
||||
@@ -1180,7 +1197,7 @@ mod tests {
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages],
|
||||
};
|
||||
|
||||
Program::new(&db, TargetVersion::Py38, search_paths);
|
||||
@@ -1578,7 +1595,7 @@ not_a_directory
|
||||
&FilePath::system("/y/src/bar.py")
|
||||
);
|
||||
let events = db.take_salsa_events();
|
||||
assert_const_function_query_was_not_run(&db, editable_install_resolution_paths, &events);
|
||||
assert_const_function_query_was_not_run(&db, dynamic_resolution_paths, &events);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1656,4 +1673,53 @@ not_a_directory
|
||||
assert!(!search_paths
|
||||
.contains(&&SearchPath::editable(db.system(), SystemPathBuf::from("/src")).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_site_packages_with_editables() {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let venv_site_packages = SystemPathBuf::from("/venv-site-packages");
|
||||
let site_packages_pth = venv_site_packages.join("foo.pth");
|
||||
let system_site_packages = SystemPathBuf::from("/system-site-packages");
|
||||
let editable_install_location = SystemPathBuf::from("/x/y/a.py");
|
||||
let system_site_packages_location = system_site_packages.join("a.py");
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all("/src")
|
||||
.unwrap();
|
||||
db.write_files([
|
||||
(&site_packages_pth, "/x/y"),
|
||||
(&editable_install_location, ""),
|
||||
(&system_site_packages_location, ""),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
Program::new(
|
||||
&db,
|
||||
TargetVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![venv_site_packages, system_site_packages],
|
||||
},
|
||||
);
|
||||
|
||||
// The editable installs discovered from the `.pth` file in the first `site-packages` directory
|
||||
// take precedence over the second `site-packages` directory...
|
||||
let a_module_name = ModuleName::new_static("a").unwrap();
|
||||
let a_module = resolve_module(&db, a_module_name.clone()).unwrap();
|
||||
assert_eq!(a_module.file().path(&db), &editable_install_location);
|
||||
|
||||
db.memory_file_system()
|
||||
.remove_file(&site_packages_pth)
|
||||
.unwrap();
|
||||
File::sync_path(&mut db, &site_packages_pth);
|
||||
|
||||
// ...But now that the `.pth` file in the first `site-packages` directory has been deleted,
|
||||
// the editable install no longer exists, so the module now resolves to the file in the
|
||||
// second `site-packages` directory
|
||||
let a_module = resolve_module(&db, a_module_name).unwrap();
|
||||
assert_eq!(a_module.file().path(&db), &system_site_packages_location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ pub(crate) struct TestCase<T> {
|
||||
pub(crate) db: TestDb,
|
||||
pub(crate) src: SystemPathBuf,
|
||||
pub(crate) stdlib: T,
|
||||
// Most test cases only ever need a single `site-packages` directory,
|
||||
// so this is a single directory instead of a `Vec` of directories,
|
||||
// like it is in `ruff_db::Program`.
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
}
|
||||
@@ -223,7 +226,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -276,7 +279,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(site_packages.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ mod tests {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
site_packages: vec![],
|
||||
custom_typeshed: None,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1515,7 +1515,7 @@ mod tests {
|
||||
SearchPathSettings {
|
||||
extra_paths: Vec::new(),
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
site_packages: vec![],
|
||||
custom_typeshed: None,
|
||||
},
|
||||
);
|
||||
@@ -1532,7 +1532,7 @@ mod tests {
|
||||
SearchPathSettings {
|
||||
extra_paths: Vec::new(),
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
site_packages: vec![],
|
||||
custom_typeshed: Some(SystemPathBuf::from(typeshed)),
|
||||
},
|
||||
);
|
||||
|
||||
32
crates/red_knot_workspace/Cargo.toml
Normal file
32
crates/red_knot_workspace/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "red_knot_workspace"
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
red_knot_module_resolver = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
crossbeam = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user