Compare commits
1 Commits
dcreager/c
...
micha/dont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcde25773d |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -14,7 +14,5 @@ crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text
|
||||
|
||||
crates/ruff_python_parser/resources/inline linguist-generated=true
|
||||
|
||||
ruff.schema.json -diff linguist-generated=true text=auto eol=lf
|
||||
crates/ruff_python_ast/src/generated.rs -diff linguist-generated=true text=auto eol=lf
|
||||
crates/ruff_python_formatter/src/generated.rs -diff linguist-generated=true text=auto eol=lf
|
||||
ruff.schema.json linguist-generated=true text=auto eol=lf
|
||||
*.md.snap linguist-language=Markdown
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -9,7 +9,6 @@
|
||||
/crates/ruff_formatter/ @MichaReiser
|
||||
/crates/ruff_python_formatter/ @MichaReiser
|
||||
/crates/ruff_python_parser/ @MichaReiser @dhruvmanila
|
||||
/crates/ruff_annotate_snippets/ @BurntSushi
|
||||
|
||||
# flake8-pyi
|
||||
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood
|
||||
|
||||
12
.github/ISSUE_TEMPLATE.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
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`).
|
||||
-->
|
||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -386,12 +386,6 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
# already checked into git.
|
||||
- run: python crates/ruff_python_ast/generate.py
|
||||
- run: python crates/ruff_python_formatter/generate.py
|
||||
- run: test -z "$(git status --porcelain)"
|
||||
# Verify that adding a plugin or rule produces clean code.
|
||||
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
|
||||
- run: cargo check
|
||||
- run: cargo fmt --all --check
|
||||
|
||||
2
.github/workflows/publish-playground.yml
vendored
2
.github/workflows/publish-playground.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.13.1
|
||||
uses: cloudflare/wrangler-action@v3.13.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
3
.github/workflows/sync_typeshed.yaml
vendored
3
.github/workflows/sync_typeshed.yaml
vendored
@@ -78,6 +78,5 @@ jobs:
|
||||
owner: "astral-sh",
|
||||
repo: "ruff",
|
||||
title: `Automated typeshed sync failed on ${new Date().toDateString()}`,
|
||||
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
labels: ["bug", "red-knot"],
|
||||
body: "Runs are listed here: https://github.com/astral-sh/ruff/actions/workflows/sync_typeshed.yaml",
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ exclude: |
|
||||
(?x)^(
|
||||
.github/workflows/release.yml|
|
||||
crates/red_knot_vendored/vendor/.*|
|
||||
crates/red_knot_project/resources/.*|
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
crates/ruff_notebook/resources/.*|
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.2
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -91,19 +91,19 @@ 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.1.1
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.31.0
|
||||
rev: 0.30.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
|
||||
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.7
|
||||
rev: v1.7.6
|
||||
hooks:
|
||||
- id: actionlint
|
||||
stages:
|
||||
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,43 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Fix typo "security_managr" to "security_manager" (`AIR303`) ([#15463](https://github.com/astral-sh/ruff/pull/15463))
|
||||
- \[`airflow`\] extend and fix AIR302 rules ([#15525](https://github.com/astral-sh/ruff/pull/15525))
|
||||
- \[`fastapi`\] Handle parameters with `Depends` correctly (`FAST003`) ([#15364](https://github.com/astral-sh/ruff/pull/15364))
|
||||
- \[`flake8-pytest-style`\] Implement pytest.warns diagnostics (`PT029`, `PT030`, `PT031`) ([#15444](https://github.com/astral-sh/ruff/pull/15444))
|
||||
- \[`flake8-pytest-style`\] Test function parameters with default arguments (`PT028`) ([#15449](https://github.com/astral-sh/ruff/pull/15449))
|
||||
- \[`flake8-type-checking`\] Avoid false positives for `|` in `TC008` ([#15201](https://github.com/astral-sh/ruff/pull/15201))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-todos`\] Allow VSCode GitHub PR extension style links in `missing-todo-link` (`TD003`) ([#15519](https://github.com/astral-sh/ruff/pull/15519))
|
||||
- \[`pyflakes`\] Show syntax error message for `F722` ([#15523](https://github.com/astral-sh/ruff/pull/15523))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Fix curly bracket spacing around f-string expressions containing curly braces ([#15471](https://github.com/astral-sh/ruff/pull/15471))
|
||||
- Fix joining of f-strings with different quotes when using quote style `Preserve` ([#15524](https://github.com/astral-sh/ruff/pull/15524))
|
||||
|
||||
### Server
|
||||
|
||||
- Avoid indexing the same workspace multiple times ([#15495](https://github.com/astral-sh/ruff/pull/15495))
|
||||
- Display context for `ruff.configuration` errors ([#15452](https://github.com/astral-sh/ruff/pull/15452))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Remove `flatten` to improve deserialization error messages ([#15414](https://github.com/astral-sh/ruff/pull/15414))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Parse triple-quoted string annotations as if parenthesized ([#15387](https://github.com/astral-sh/ruff/pull/15387))
|
||||
- \[`fastapi`\] Update `Annotated` fixes (`FAST002`) ([#15462](https://github.com/astral-sh/ruff/pull/15462))
|
||||
- \[`flake8-bandit`\] Check for `builtins` instead of `builtin` (`S102`, `PTH123`) ([#15443](https://github.com/astral-sh/ruff/pull/15443))
|
||||
- \[`flake8-pathlib`\] Fix `--select` for `os-path-dirname` (`PTH120`) ([#15446](https://github.com/astral-sh/ruff/pull/15446))
|
||||
- \[`ruff`\] Fix false positive on global keyword (`RUF052`) ([#15235](https://github.com/astral-sh/ruff/pull/15235))
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Preview features
|
||||
|
||||
525
Cargo.lock
generated
525
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -13,7 +13,6 @@ license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
ruff = { path = "crates/ruff" }
|
||||
ruff_annotate_snippets = { path = "crates/ruff_annotate_snippets" }
|
||||
ruff_cache = { path = "crates/ruff_cache" }
|
||||
ruff_db = { path = "crates/ruff_db", default-features = false }
|
||||
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
|
||||
@@ -41,11 +40,10 @@ ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
red_knot_workspace = { path = "crates/red_knot_workspace", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
anstream = { version = "0.6.18" }
|
||||
anstyle = { version = "1.0.10" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
anyhow = { version = "1.0.80" }
|
||||
assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
@@ -59,7 +57,7 @@ clap = { version = "4.5.3", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.6.0" }
|
||||
clearscreen = { version = "4.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
|
||||
colored = { version = "3.0.0" }
|
||||
colored = { version = "2.1.0" }
|
||||
console_error_panic_hook = { version = "0.1.7" }
|
||||
console_log = { version = "1.0.0" }
|
||||
countme = { version = "3.0.1" }
|
||||
@@ -105,7 +103,7 @@ matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "8.0.0" }
|
||||
notify = { version = "7.0.0" }
|
||||
ordermap = { version = "0.5.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
@@ -134,7 +132,6 @@ serde_with = { version = "3.6.0", default-features = false, features = [
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
smallvec = { version = "1.13.2" }
|
||||
snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] }
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.26.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.26.0" }
|
||||
@@ -152,7 +149,6 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features =
|
||||
"fmt",
|
||||
] }
|
||||
tracing-tree = { version = "0.4.0" }
|
||||
tryfn = { version = "0.2.1" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
unicode-ident = { version = "1.0.12" }
|
||||
|
||||
@@ -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.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.1/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.2
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
16
_typos.toml
16
_typos.toml
@@ -1,9 +1,10 @@
|
||||
[files]
|
||||
# https://github.com/crate-ci/typos/issues/868
|
||||
extend-exclude = [
|
||||
"crates/red_knot_vendored/vendor/**/*",
|
||||
"**/resources/**/*",
|
||||
"**/snapshots/**/*",
|
||||
"crates/red_knot_vendored/vendor/**/*",
|
||||
"**/resources/**/*",
|
||||
"**/snapshots/**/*",
|
||||
"crates/red_knot_workspace/src/workspace/pyproject/package_name.rs"
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
@@ -20,10 +21,7 @@ Numer = "Numer" # Library name 'NumerBlox' in "Who's Using Ruff?"
|
||||
|
||||
[default]
|
||||
extend-ignore-re = [
|
||||
# Line ignore with trailing "spellchecker:disable-line"
|
||||
"(?Rm)^.*#\\s*spellchecker:disable-line$",
|
||||
"LICENSEs",
|
||||
# Line ignore with trailing "spellchecker:disable-line"
|
||||
"(?Rm)^.*#\\s*spellchecker:disable-line$",
|
||||
"LICENSEs",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
"FrIeNdLy" = "FrIeNdLy"
|
||||
|
||||
@@ -13,7 +13,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_project = { workspace = true, features = ["zstd"] }
|
||||
red_knot_workspace = { workspace = true, features = ["zstd"] }
|
||||
red_knot_server = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
|
||||
@@ -32,14 +32,9 @@ tracing-flame = { workspace = true }
|
||||
tracing-tree = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
insta-cmd = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -6,11 +6,13 @@ use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use python_version::PythonVersion;
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::watch;
|
||||
use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
@@ -69,28 +71,31 @@ struct Args {
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn to_options(&self, cli_cwd: &SystemPath) -> Options {
|
||||
Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: self.python_version.map(Into::into),
|
||||
venv_path: self
|
||||
.venv_path
|
||||
.as_ref()
|
||||
.map(|venv_path| SystemPath::absolute(venv_path, cli_cwd)),
|
||||
typeshed: self
|
||||
.typeshed
|
||||
.as_ref()
|
||||
.map(|typeshed| SystemPath::absolute(typeshed, cli_cwd)),
|
||||
extra_paths: self.extra_search_path.as_ref().map(|extra_search_paths| {
|
||||
extra_search_paths
|
||||
.iter()
|
||||
.map(|path| SystemPath::absolute(path, cli_cwd))
|
||||
.collect()
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Default::default()
|
||||
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
|
||||
let mut configuration = Configuration::default();
|
||||
|
||||
if let Some(python_version) = self.python_version {
|
||||
configuration.python_version = Some(python_version.into());
|
||||
}
|
||||
|
||||
if let Some(venv_path) = &self.venv_path {
|
||||
configuration.search_paths.site_packages = Some(SitePackages::Derived {
|
||||
venv_path: SystemPath::absolute(venv_path, cli_cwd),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(typeshed) = &self.typeshed {
|
||||
configuration.search_paths.typeshed = Some(SystemPath::absolute(typeshed, cli_cwd));
|
||||
}
|
||||
|
||||
if let Some(extra_search_paths) = &self.extra_search_path {
|
||||
configuration.search_paths.extra_paths = extra_search_paths
|
||||
.iter()
|
||||
.map(|path| Some(SystemPath::absolute(path, cli_cwd)))
|
||||
.collect();
|
||||
}
|
||||
|
||||
configuration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,13 +164,18 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
.unwrap_or_else(|| cli_base_path.clone());
|
||||
|
||||
let system = OsSystem::new(cwd.clone());
|
||||
let cli_options = args.to_options(&cwd);
|
||||
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
workspace_metadata.apply_cli_options(cli_options.clone());
|
||||
let cli_configuration = args.to_configuration(&cwd);
|
||||
let workspace_metadata = WorkspaceMetadata::discover(
|
||||
system.current_directory(),
|
||||
&system,
|
||||
Some(&cli_configuration),
|
||||
)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
|
||||
// TODO: Use the `program_settings` to compute the key for the database's persistent
|
||||
// cache and load the cache if it exists.
|
||||
let mut db = RootDatabase::new(workspace_metadata, system)?;
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_configuration);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -216,13 +226,13 @@ struct MainLoop {
|
||||
receiver: crossbeam_channel::Receiver<MainLoopMessage>,
|
||||
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<ProjectWatcher>,
|
||||
watcher: Option<WorkspaceWatcher>,
|
||||
|
||||
cli_options: Options,
|
||||
cli_configuration: Configuration,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
fn new(cli_configuration: Configuration) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
@@ -230,27 +240,27 @@ impl MainLoop {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
cli_configuration,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
}
|
||||
|
||||
fn watch(mut self, db: &mut ProjectDatabase) -> anyhow::Result<ExitStatus> {
|
||||
fn watch(mut self, db: &mut RootDatabase) -> anyhow::Result<ExitStatus> {
|
||||
tracing::debug!("Starting watch mode");
|
||||
let sender = self.sender.clone();
|
||||
let watcher = watch::directory_watcher(move |event| {
|
||||
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
|
||||
})?;
|
||||
|
||||
self.watcher = Some(ProjectWatcher::new(watcher, db));
|
||||
self.watcher = Some(WorkspaceWatcher::new(watcher, db));
|
||||
|
||||
self.run(db);
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn run(mut self, db: &mut ProjectDatabase) -> ExitStatus {
|
||||
fn run(mut self, db: &mut RootDatabase) -> ExitStatus {
|
||||
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
|
||||
|
||||
let result = self.main_loop(db);
|
||||
@@ -260,7 +270,7 @@ impl MainLoop {
|
||||
result
|
||||
}
|
||||
|
||||
fn main_loop(&mut self, db: &mut ProjectDatabase) -> ExitStatus {
|
||||
fn main_loop(&mut self, db: &mut RootDatabase) -> ExitStatus {
|
||||
// Schedule the first check.
|
||||
tracing::debug!("Starting main loop");
|
||||
|
||||
@@ -272,7 +282,7 @@ impl MainLoop {
|
||||
let db = db.clone();
|
||||
let sender = self.sender.clone();
|
||||
|
||||
// Spawn a new task that checks the project. This needs to be done in a separate thread
|
||||
// Spawn a new task that checks the workspace. This needs to be done in a separate thread
|
||||
// to prevent blocking the main loop here.
|
||||
rayon::spawn(move || {
|
||||
if let Ok(result) = db.check() {
|
||||
@@ -314,7 +324,7 @@ impl MainLoop {
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes, Some(&self.cli_options));
|
||||
db.apply_changes(changes, Some(&self.cli_configuration));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Specifying an option on the CLI should take precedence over the same setting in the
|
||||
/// project's configuration.
|
||||
#[test]
|
||||
fn test_config_override() -> anyhow::Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
std::fs::write(
|
||||
tempdir.path().join("pyproject.toml"),
|
||||
r#"
|
||||
[tool.knot.environment]
|
||||
python-version = "3.11"
|
||||
"#,
|
||||
)
|
||||
.context("Failed to write settings")?;
|
||||
|
||||
std::fs::write(
|
||||
tempdir.path().join("test.py"),
|
||||
r#"
|
||||
import sys
|
||||
|
||||
# Access `sys.last_exc` that was only added in Python 3.12
|
||||
print(sys.last_exc)
|
||||
"#,
|
||||
)
|
||||
.context("Failed to write test.py")?;
|
||||
|
||||
insta::with_settings!({filters => vec![(&*tempdir_filter(&tempdir), "<temp_dir>/")]}, {
|
||||
assert_cmd_snapshot!(knot().arg("--project").arg(tempdir.path()), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
assert_cmd_snapshot!(knot().arg("--project").arg(tempdir.path()).arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn knot() -> Command {
|
||||
Command::new(get_cargo_bin("red_knot"))
|
||||
}
|
||||
|
||||
fn tempdir_filter(tempdir: &TempDir) -> String {
|
||||
format!(r"{}\\?/?", regex::escape(tempdir.path().to_str().unwrap()))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,190 +0,0 @@
|
||||
use std::{collections::HashMap, hash::BuildHasher};
|
||||
|
||||
use red_knot_python_semantic::{PythonPlatform, PythonVersion, SitePackages};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
|
||||
/// Combine two values, preferring the values in `self`.
|
||||
///
|
||||
/// The logic should follow that of Cargo's `config.toml`:
|
||||
///
|
||||
/// > If a key is specified in multiple config files, the values will get merged together.
|
||||
/// > Numbers, strings, and booleans will use the value in the deeper config directory taking
|
||||
/// > precedence over ancestor directories, where the home directory is the lowest priority.
|
||||
/// > Arrays will be joined together with higher precedence items being placed later in the
|
||||
/// > merged array.
|
||||
///
|
||||
/// ## uv Compatibility
|
||||
///
|
||||
/// The merging behavior differs from uv in that values with higher precedence in arrays
|
||||
/// are placed later in the merged array. This is because we want to support overriding
|
||||
/// earlier values and values from other configurations, including unsetting them.
|
||||
/// For example: patterns coming last in file inclusion and exclusion patterns
|
||||
/// allow overriding earlier patterns, matching the `gitignore` behavior.
|
||||
/// Generally speaking, it feels more intuitive if later values override earlier values
|
||||
/// than the other way around: `knot --exclude png --exclude "!important.png"`.
|
||||
///
|
||||
/// The main downside of this approach is that the ordering can be surprising in cases
|
||||
/// where the option has a "first match" semantic and not a "last match" wins.
|
||||
/// One such example is `extra-paths` where the semantics is given by Python:
|
||||
/// the module on the first matching search path wins.
|
||||
///
|
||||
/// ```toml
|
||||
/// [environment]
|
||||
/// extra-paths = ["b", "c"]
|
||||
/// ```
|
||||
///
|
||||
/// ```bash
|
||||
/// knot --extra-paths a
|
||||
/// ```
|
||||
///
|
||||
/// That's why a user might expect that this configuration results in `["a", "b", "c"]`,
|
||||
/// because the CLI has higher precedence. However, the current implementation results in a
|
||||
/// resolved extra search path of `["b", "c", "a"]`, which means `a` will be tried last.
|
||||
///
|
||||
/// There's an argument here that the user should be able to specify the order of the paths,
|
||||
/// because only then is the user in full control of where to insert the path when specyifing `extra-paths`
|
||||
/// in multiple sources.
|
||||
///
|
||||
/// ## Macro
|
||||
/// You can automatically derive `Combine` for structs with named fields by using `derive(ruff_macros::Combine)`.
|
||||
pub trait Combine {
|
||||
#[must_use]
|
||||
fn combine(mut self, other: Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.combine_with(other);
|
||||
self
|
||||
}
|
||||
|
||||
fn combine_with(&mut self, other: Self);
|
||||
}
|
||||
|
||||
impl<T> Combine for Option<T>
|
||||
where
|
||||
T: Combine,
|
||||
{
|
||||
fn combine(self, other: Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match (self, other) {
|
||||
(Some(a), Some(b)) => Some(a.combine(b)),
|
||||
(None, Some(b)) => Some(b),
|
||||
(a, _) => a,
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_with(&mut self, other: Self) {
|
||||
match (self, other) {
|
||||
(Some(a), Some(b)) => {
|
||||
a.combine_with(b);
|
||||
}
|
||||
(a @ None, Some(b)) => {
|
||||
*a = Some(b);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Combine for Vec<T> {
|
||||
fn combine_with(&mut self, mut other: Self) {
|
||||
// `self` takes precedence over `other` but values with higher precedence must be placed after.
|
||||
// Swap the vectors so that `other` is the one that gets extended, so that the values of `self` come after.
|
||||
std::mem::swap(self, &mut other);
|
||||
self.extend(other);
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, S> Combine for HashMap<K, V, S>
|
||||
where
|
||||
K: Eq + std::hash::Hash,
|
||||
S: BuildHasher,
|
||||
{
|
||||
fn combine_with(&mut self, mut other: Self) {
|
||||
// `self` takes precedence over `other` but `extend` overrides existing values.
|
||||
// Swap the hash maps so that `self` is the one that gets extended.
|
||||
std::mem::swap(self, &mut other);
|
||||
self.extend(other);
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [`Combine`] for a value that always returns `self` when combined with another value.
|
||||
macro_rules! impl_noop_combine {
|
||||
($name:ident) => {
|
||||
impl Combine for $name {
|
||||
#[inline(always)]
|
||||
fn combine_with(&mut self, _other: Self) {}
|
||||
|
||||
#[inline(always)]
|
||||
fn combine(self, _other: Self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_noop_combine!(SystemPathBuf);
|
||||
impl_noop_combine!(PythonPlatform);
|
||||
impl_noop_combine!(SitePackages);
|
||||
impl_noop_combine!(PythonVersion);
|
||||
|
||||
// std types
|
||||
impl_noop_combine!(bool);
|
||||
impl_noop_combine!(usize);
|
||||
impl_noop_combine!(u8);
|
||||
impl_noop_combine!(u16);
|
||||
impl_noop_combine!(u32);
|
||||
impl_noop_combine!(u64);
|
||||
impl_noop_combine!(u128);
|
||||
impl_noop_combine!(isize);
|
||||
impl_noop_combine!(i8);
|
||||
impl_noop_combine!(i16);
|
||||
impl_noop_combine!(i32);
|
||||
impl_noop_combine!(i64);
|
||||
impl_noop_combine!(i128);
|
||||
impl_noop_combine!(String);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::combine::Combine;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn combine_option() {
|
||||
assert_eq!(Some(1).combine(Some(2)), Some(1));
|
||||
assert_eq!(None.combine(Some(2)), Some(2));
|
||||
assert_eq!(Some(1).combine(None), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_vec() {
|
||||
assert_eq!(None.combine(Some(vec![1, 2, 3])), Some(vec![1, 2, 3]));
|
||||
assert_eq!(Some(vec![1, 2, 3]).combine(None), Some(vec![1, 2, 3]));
|
||||
assert_eq!(
|
||||
Some(vec![1, 2, 3]).combine(Some(vec![4, 5, 6])),
|
||||
Some(vec![4, 5, 6, 1, 2, 3])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_map() {
|
||||
let a: HashMap<u32, _> = HashMap::from_iter([(1, "a"), (2, "a"), (3, "a")]);
|
||||
let b: HashMap<u32, _> = HashMap::from_iter([(0, "b"), (2, "b"), (5, "b")]);
|
||||
|
||||
assert_eq!(None.combine(Some(b.clone())), Some(b.clone()));
|
||||
assert_eq!(Some(a.clone()).combine(None), Some(a.clone()));
|
||||
assert_eq!(
|
||||
Some(a).combine(Some(b)),
|
||||
Some(HashMap::from_iter([
|
||||
(0, "b"),
|
||||
// The value from `a` takes precedence
|
||||
(1, "a"),
|
||||
(2, "a"),
|
||||
(3, "a"),
|
||||
(5, "b")
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,470 +0,0 @@
|
||||
#![allow(clippy::ref_option)]
|
||||
|
||||
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
|
||||
use red_knot_python_semantic::register_lints;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
use ruff_db::system::walk_directory::WalkState;
|
||||
use ruff_db::system::{FileType, SystemPath};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use db::{Db, ProjectDatabase};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
|
||||
pub mod combine;
|
||||
|
||||
mod db;
|
||||
mod files;
|
||||
pub mod metadata;
|
||||
pub mod watch;
|
||||
|
||||
pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock<LintRegistry> =
|
||||
std::sync::LazyLock::new(default_lints_registry);
|
||||
|
||||
pub fn default_lints_registry() -> LintRegistry {
|
||||
let mut builder = LintRegistryBuilder::default();
|
||||
register_lints(&mut builder);
|
||||
builder.build()
|
||||
}
|
||||
|
||||
/// The project as a Salsa ingredient.
|
||||
///
|
||||
/// ## How is a project different from a program?
|
||||
/// There are two (related) motivations:
|
||||
///
|
||||
/// 1. Program is defined in `ruff_db` and it can't reference the settings types for the linter and formatter
|
||||
/// without introducing a cyclic dependency. The project is defined in a higher level crate
|
||||
/// where it can reference these setting types.
|
||||
/// 2. Running `ruff check` with different target versions results in different programs (settings) but
|
||||
/// it remains the same project. That's why program is a narrowed view of the project only
|
||||
/// holding on to the most fundamental settings required for checking.
|
||||
#[salsa::input]
|
||||
pub struct Project {
|
||||
/// The files that are open in the project.
|
||||
///
|
||||
/// Setting the open files to a non-`None` value changes `check` to only check the
|
||||
/// open files rather than all files in the project.
|
||||
#[return_ref]
|
||||
#[default]
|
||||
open_fileset: Option<Arc<FxHashSet<File>>>,
|
||||
|
||||
/// The first-party files of this project.
|
||||
#[default]
|
||||
#[return_ref]
|
||||
file_set: IndexedFiles,
|
||||
|
||||
/// The metadata describing the project, including the unresolved options.
|
||||
#[return_ref]
|
||||
pub metadata: ProjectMetadata,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {
|
||||
Project::builder(metadata)
|
||||
.durability(Durability::MEDIUM)
|
||||
.open_fileset_durability(Durability::LOW)
|
||||
.file_set_durability(Durability::LOW)
|
||||
.new(db)
|
||||
}
|
||||
|
||||
pub fn root(self, db: &dyn Db) -> &SystemPath {
|
||||
self.metadata(db).root()
|
||||
}
|
||||
|
||||
pub fn name(self, db: &dyn Db) -> &str {
|
||||
self.metadata(db).name()
|
||||
}
|
||||
|
||||
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
|
||||
tracing::debug!("Reloading project");
|
||||
assert_eq!(self.root(db), metadata.root());
|
||||
|
||||
if &metadata != self.metadata(db) {
|
||||
self.set_metadata(db).to(metadata);
|
||||
}
|
||||
|
||||
self.reload_files(db);
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
let result = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let inner_result = Arc::clone(&result);
|
||||
|
||||
let db = db.clone();
|
||||
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();
|
||||
let project_span = project_span.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", file=%file.path(&db));
|
||||
let _entered = check_file_span.entered();
|
||||
|
||||
let file_diagnostics = check_file(&db, file);
|
||||
result.lock().unwrap().extend(file_diagnostics);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
/// Opens a file in the project.
|
||||
///
|
||||
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
|
||||
pub fn open_file(self, db: &mut dyn Db, file: File) {
|
||||
tracing::debug!("Opening file `{}`", file.path(db));
|
||||
|
||||
let mut open_files = self.take_open_files(db);
|
||||
open_files.insert(file);
|
||||
self.set_open_files(db, open_files);
|
||||
}
|
||||
|
||||
/// Closes a file in the project.
|
||||
pub fn close_file(self, db: &mut dyn Db, file: File) -> bool {
|
||||
tracing::debug!("Closing file `{}`", file.path(db));
|
||||
|
||||
let mut open_files = self.take_open_files(db);
|
||||
let removed = open_files.remove(&file);
|
||||
|
||||
if removed {
|
||||
self.set_open_files(db, open_files);
|
||||
}
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Sets the open files in the project.
|
||||
///
|
||||
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet<File>) {
|
||||
tracing::debug!("Set open project files (count: {})", open_files.len());
|
||||
|
||||
self.set_open_fileset(db).to(Some(Arc::new(open_files)));
|
||||
}
|
||||
|
||||
/// This takes the open files from the project and returns them.
|
||||
///
|
||||
/// This changes the behavior of `check` to check all files in the project instead of just the open files.
|
||||
fn take_open_files(self, db: &mut dyn Db) -> FxHashSet<File> {
|
||||
tracing::debug!("Take open project files");
|
||||
|
||||
// Salsa will cancel any pending queries and remove its own reference to `open_files`
|
||||
// so that the reference counter to `open_files` now drops to 1.
|
||||
let open_files = self.set_open_fileset(db).to(None);
|
||||
|
||||
if let Some(open_files) = open_files {
|
||||
Arc::try_unwrap(open_files).unwrap()
|
||||
} else {
|
||||
FxHashSet::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the file is open in the project.
|
||||
///
|
||||
/// A file is considered open when:
|
||||
/// * explicitly set as an open file using [`open_file`](Self::open_file)
|
||||
/// * It has a [`SystemPath`] and belongs to a package's `src` files
|
||||
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath)
|
||||
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool {
|
||||
if let Some(open_files) = self.open_files(db) {
|
||||
open_files.contains(&file)
|
||||
} else if file.path(db).is_system_path() {
|
||||
self.contains_file(db, file)
|
||||
} else {
|
||||
file.path(db).is_system_virtual_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `file` is a first-party file part of this package.
|
||||
pub fn contains_file(self, db: &dyn Db, file: File) -> bool {
|
||||
self.files(db).contains(&file)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub fn remove_file(self, db: &mut dyn Db, file: File) {
|
||||
tracing::debug!(
|
||||
"Removing file `{}` from project `{}`",
|
||||
file.path(db),
|
||||
self.name(db)
|
||||
);
|
||||
|
||||
let Some(mut index) = IndexedFiles::indexed_mut(db, self) else {
|
||||
return;
|
||||
};
|
||||
|
||||
index.remove(file);
|
||||
}
|
||||
|
||||
pub fn add_file(self, db: &mut dyn Db, file: File) {
|
||||
tracing::debug!(
|
||||
"Adding file `{}` to project `{}`",
|
||||
file.path(db),
|
||||
self.name(db)
|
||||
);
|
||||
|
||||
let Some(mut index) = IndexedFiles::indexed_mut(db, self) else {
|
||||
return;
|
||||
};
|
||||
|
||||
index.insert(file);
|
||||
}
|
||||
|
||||
/// Returns the files belonging to this project.
|
||||
pub fn files(self, db: &dyn Db) -> Indexed<'_> {
|
||||
let files = self.file_set(db);
|
||||
|
||||
let indexed = match files.get() {
|
||||
Index::Lazy(vacant) => {
|
||||
let _entered =
|
||||
tracing::debug_span!("Project::index_files", package = %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)
|
||||
}
|
||||
Index::Indexed(indexed) => indexed,
|
||||
};
|
||||
|
||||
indexed
|
||||
}
|
||||
|
||||
pub fn reload_files(self, db: &mut dyn Db) {
|
||||
tracing::debug!("Reloading files for project `{}`", self.name(db));
|
||||
|
||||
if !self.file_set(db).is_lazy() {
|
||||
// Force a re-index of the files in the next revision.
|
||||
self.set_file_set(db).to(IndexedFiles::lazy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = 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(),
|
||||
}));
|
||||
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()));
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
|
||||
|
||||
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>),
|
||||
Indexed(files::Indexed<'a>),
|
||||
}
|
||||
|
||||
impl<'a> ProjectFiles<'a> {
|
||||
fn new(db: &'a dyn Db, project: Project) -> Self {
|
||||
if let Some(open_files) = project.open_files(db) {
|
||||
ProjectFiles::OpenFiles(open_files)
|
||||
} else {
|
||||
ProjectFiles::Indexed(project.files(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a ProjectFiles<'a> {
|
||||
type Item = File;
|
||||
type IntoIter = ProjectFilesIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
match self {
|
||||
ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()),
|
||||
ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed {
|
||||
files: indexed.into_iter(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ProjectFilesIter<'db> {
|
||||
OpenFiles(std::collections::hash_set::Iter<'db, File>),
|
||||
Indexed { files: files::IndexedIter<'db> },
|
||||
}
|
||||
|
||||
impl Iterator for ProjectFilesIter<'_> {
|
||||
type Item = File;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
ProjectFilesIter::OpenFiles(files) => files.next().copied(),
|
||||
ProjectFilesIter::Indexed { files } => files.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IOErrorDiagnostic {
|
||||
file: File,
|
||||
error: SourceTextError,
|
||||
}
|
||||
|
||||
impl Diagnostic for IOErrorDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
None
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file, ProjectMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
#[test]
|
||||
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||
let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/"));
|
||||
let mut db = TestDb::new(project);
|
||||
let path = SystemPath::new("test.py");
|
||||
|
||||
db.write_file(path, "x = 10")?;
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
// Now the file gets deleted before we had a chance to read its source text.
|
||||
db.memory_file_system().remove_file(path)?;
|
||||
file.sync(&mut db);
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
);
|
||||
|
||||
let events = db.take_salsa_events();
|
||||
assert_function_query_was_not_run(&db, check_types, file, &events);
|
||||
|
||||
// The user now creates a new file with an empty text. The source text
|
||||
// content returned by `source_text` remains unchanged, but the diagnostics should get updated.
|
||||
db.write_file(path, "").unwrap();
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![] as Vec<String>
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
use red_knot_python_semantic::ProgramSettings;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::name::Name;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::combine::Combine;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError};
|
||||
use options::KnotTomlError;
|
||||
use options::Options;
|
||||
|
||||
pub mod options;
|
||||
pub mod pyproject;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(serde::Serialize))]
|
||||
pub struct ProjectMetadata {
|
||||
pub(super) name: Name,
|
||||
|
||||
pub(super) root: SystemPathBuf,
|
||||
|
||||
/// The raw options
|
||||
pub(super) options: Options,
|
||||
}
|
||||
|
||||
impl ProjectMetadata {
|
||||
/// Creates a project with the given name and root that uses the default options.
|
||||
pub fn new(name: Name, root: SystemPathBuf) -> Self {
|
||||
Self {
|
||||
name,
|
||||
root,
|
||||
options: Options::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a project from a `pyproject.toml` file.
|
||||
pub(crate) fn from_pyproject(pyproject: PyProject, root: SystemPathBuf) -> Self {
|
||||
Self::from_options(
|
||||
pyproject
|
||||
.tool
|
||||
.and_then(|tool| tool.knot)
|
||||
.unwrap_or_default(),
|
||||
root,
|
||||
pyproject.project.as_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads a project from a set of options with an optional pyproject-project table.
|
||||
pub(crate) fn from_options(
|
||||
options: Options,
|
||||
root: SystemPathBuf,
|
||||
project: Option<&Project>,
|
||||
) -> Self {
|
||||
let name = project
|
||||
.and_then(|project| project.name.as_ref())
|
||||
.map(|name| Name::new(&**name))
|
||||
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
|
||||
|
||||
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python
|
||||
Self {
|
||||
name,
|
||||
root,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovers the closest project at `path` and returns its metadata.
|
||||
///
|
||||
/// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence
|
||||
/// the resolve the project's root.
|
||||
///
|
||||
/// 1. The closest `pyproject.toml` with a `tool.knot` section or `knot.toml`.
|
||||
/// 1. The closest `pyproject.toml`.
|
||||
/// 1. Fallback to use `path` as the root and use the default settings.
|
||||
pub fn discover(
|
||||
path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> Result<ProjectMetadata, ProjectDiscoveryError> {
|
||||
tracing::debug!("Searching for a project in '{path}'");
|
||||
|
||||
if !system.is_directory(path) {
|
||||
return Err(ProjectDiscoveryError::NotADirectory(path.to_path_buf()));
|
||||
}
|
||||
|
||||
let mut closest_project: Option<ProjectMetadata> = None;
|
||||
|
||||
for project_root in path.ancestors() {
|
||||
let pyproject_path = project_root.join("pyproject.toml");
|
||||
|
||||
let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) {
|
||||
match PyProject::from_toml_str(&pyproject_str) {
|
||||
Ok(pyproject) => Some(pyproject),
|
||||
Err(error) => {
|
||||
return Err(ProjectDiscoveryError::InvalidPyProject {
|
||||
path: pyproject_path,
|
||||
source: Box::new(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// A `knot.toml` takes precedence over a `pyproject.toml`.
|
||||
let knot_toml_path = project_root.join("knot.toml");
|
||||
if let Ok(knot_str) = system.read_to_string(&knot_toml_path) {
|
||||
let options = match Options::from_toml_str(&knot_str) {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return Err(ProjectDiscoveryError::InvalidKnotToml {
|
||||
path: knot_toml_path,
|
||||
source: Box::new(error),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if pyproject
|
||||
.as_ref()
|
||||
.is_some_and(|project| project.knot().is_some())
|
||||
{
|
||||
// TODO: Consider using a diagnostic here
|
||||
tracing::warn!("Ignoring the `tool.knot` section in `{pyproject_path}` because `{knot_toml_path}` takes precedence.");
|
||||
}
|
||||
|
||||
tracing::debug!("Found project at '{}'", project_root);
|
||||
return Ok(ProjectMetadata::from_options(
|
||||
options,
|
||||
project_root.to_path_buf(),
|
||||
pyproject
|
||||
.as_ref()
|
||||
.and_then(|pyproject| pyproject.project.as_ref()),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(pyproject) = pyproject {
|
||||
let has_knot_section = pyproject.knot().is_some();
|
||||
let metadata =
|
||||
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf());
|
||||
|
||||
if has_knot_section {
|
||||
tracing::debug!("Found project at '{}'", project_root);
|
||||
|
||||
return Ok(metadata);
|
||||
}
|
||||
|
||||
// Not a project itself, keep looking for an enclosing project.
|
||||
if closest_project.is_none() {
|
||||
closest_project = Some(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No project found, but maybe a pyproject.toml was found.
|
||||
let metadata = if let Some(closest_project) = closest_project {
|
||||
tracing::debug!(
|
||||
"Project without `tool.knot` section: '{}'",
|
||||
closest_project.root()
|
||||
);
|
||||
|
||||
closest_project
|
||||
} else {
|
||||
tracing::debug!("The ancestor directories contain no `pyproject.toml`. Falling back to a virtual project.");
|
||||
|
||||
// Create a project with a default configuration
|
||||
Self::new(
|
||||
path.file_name().unwrap_or("root").into(),
|
||||
path.to_path_buf(),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &SystemPath {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &Options {
|
||||
&self.options
|
||||
}
|
||||
|
||||
pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings {
|
||||
self.options.to_program_settings(self.root(), system)
|
||||
}
|
||||
|
||||
/// Combine the project options with the CLI options where the CLI options take precedence.
|
||||
pub fn apply_cli_options(&mut self, options: Options) {
|
||||
self.options = options.combine(std::mem::take(&mut self.options));
|
||||
}
|
||||
|
||||
/// Combine the project options with the user options where project options take precedence.
|
||||
pub fn apply_user_options(&mut self, options: Options) {
|
||||
self.options.combine_with(options);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProjectDiscoveryError {
|
||||
#[error("project path '{0}' is not a directory")]
|
||||
NotADirectory(SystemPathBuf),
|
||||
|
||||
#[error("{path} is not a valid `pyproject.toml`: {source}")]
|
||||
InvalidPyProject {
|
||||
source: Box<PyProjectError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
|
||||
#[error("{path} is not a valid `knot.toml`: {source}")]
|
||||
InvalidKnotToml {
|
||||
source: Box<KnotTomlError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Integration tests for project discovery
|
||||
|
||||
use crate::snapshot_project;
|
||||
use anyhow::{anyhow, Context};
|
||||
use insta::assert_ron_snapshot;
|
||||
use ruff_db::system::{SystemPathBuf, TestSystem};
|
||||
|
||||
use crate::{ProjectDiscoveryError, ProjectMetadata};
|
||||
|
||||
#[test]
|
||||
fn project_without_pyproject() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([(root.join("foo.py"), ""), (root.join("bar.py"), "")])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let project =
|
||||
ProjectMetadata::discover(&root, &system).context("Failed to discover project")?;
|
||||
|
||||
assert_eq!(project.root(), &*root);
|
||||
|
||||
snapshot_project!(project);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_pyproject() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "backend"
|
||||
|
||||
"#,
|
||||
),
|
||||
(root.join("db/__init__.py"), ""),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let project =
|
||||
ProjectMetadata::discover(&root, &system).context("Failed to discover project")?;
|
||||
|
||||
assert_eq!(project.root(), &*root);
|
||||
snapshot_project!(project);
|
||||
|
||||
// Discovering the same package from a subdirectory should give the same result
|
||||
let from_src = ProjectMetadata::discover(&root.join("db"), &system)
|
||||
.context("Failed to discover project from src sub-directory")?;
|
||||
|
||||
assert_eq!(from_src, project);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_with_invalid_pyproject() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "backend"
|
||||
|
||||
[tool.knot
|
||||
"#,
|
||||
),
|
||||
(root.join("db/__init__.py"), ""),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let Err(error) = ProjectMetadata::discover(&root, &system) else {
|
||||
return Err(anyhow!("Expected project discovery to fail because of invalid syntax in the pyproject.toml"));
|
||||
};
|
||||
|
||||
assert_error_eq(
|
||||
&error,
|
||||
r#"/app/pyproject.toml is not a valid `pyproject.toml`: TOML parse error at line 5, column 31
|
||||
|
|
||||
5 | [tool.knot
|
||||
| ^
|
||||
invalid table header
|
||||
expected `.`, `]`
|
||||
"#,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_projects_in_sub_project() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "project-root"
|
||||
|
||||
[tool.knot.src]
|
||||
root = "src"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
root.join("packages/a/pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "nested-project"
|
||||
|
||||
[tool.knot.src]
|
||||
root = "src"
|
||||
"#,
|
||||
),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
|
||||
|
||||
snapshot_project!(sub_project);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_projects_in_root_project() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "project-root"
|
||||
|
||||
[tool.knot.src]
|
||||
root = "src"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
root.join("packages/a/pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "nested-project"
|
||||
|
||||
[tool.knot.src]
|
||||
root = "src"
|
||||
"#,
|
||||
),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let root = ProjectMetadata::discover(&root, &system)?;
|
||||
|
||||
snapshot_project!(root);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_projects_without_knot_sections() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "project-root"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
root.join("packages/a/pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "nested-project"
|
||||
"#,
|
||||
),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
|
||||
|
||||
snapshot_project!(sub_project);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_projects_with_outer_knot_section() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "project-root"
|
||||
|
||||
[tool.knot.environment]
|
||||
python-version = "3.10"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
root.join("packages/a/pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "nested-project"
|
||||
"#,
|
||||
),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let root = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
|
||||
|
||||
snapshot_project!(root);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A `knot.toml` takes precedence over any `pyproject.toml`.
|
||||
///
|
||||
/// However, the `pyproject.toml` is still loaded to get the project name and, in the future,
|
||||
/// the requires-python constraint.
|
||||
#[test]
|
||||
fn project_with_knot_and_pyproject_toml() -> anyhow::Result<()> {
|
||||
let system = TestSystem::default();
|
||||
let root = SystemPathBuf::from("/app");
|
||||
|
||||
system
|
||||
.memory_file_system()
|
||||
.write_files([
|
||||
(
|
||||
root.join("pyproject.toml"),
|
||||
r#"
|
||||
[project]
|
||||
name = "super-app"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[tool.knot.src]
|
||||
root = "this_option_is_ignored"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
root.join("knot.toml"),
|
||||
r#"
|
||||
[src]
|
||||
root = "src"
|
||||
"#,
|
||||
),
|
||||
])
|
||||
.context("Failed to write files")?;
|
||||
|
||||
let root = ProjectMetadata::discover(&root, &system)?;
|
||||
|
||||
snapshot_project!(root);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) {
|
||||
assert_eq!(error.to_string().replace('\\', "/"), message);
|
||||
}
|
||||
|
||||
/// Snapshots a project but with all paths using unix separators.
|
||||
#[macro_export]
|
||||
macro_rules! snapshot_project {
|
||||
($project:expr) => {{
|
||||
assert_ron_snapshot!($project,{
|
||||
".root" => insta::dynamic_redaction(|content, _content_path| {
|
||||
content.as_str().unwrap().replace("\\", "/")
|
||||
}),
|
||||
});
|
||||
}};
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
use red_knot_python_semantic::{
|
||||
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
|
||||
};
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_macros::Combine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
/// The options for the project.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct Options {
|
||||
pub environment: Option<EnvironmentOptions>,
|
||||
|
||||
pub src: Option<SrcOptions>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub(crate) fn from_toml_str(content: &str) -> Result<Self, KnotTomlError> {
|
||||
let options = toml::from_str(content)?;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
pub(crate) fn to_program_settings(
|
||||
&self,
|
||||
project_root: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> ProgramSettings {
|
||||
let (python_version, python_platform) = self
|
||||
.environment
|
||||
.as_ref()
|
||||
.map(|env| (env.python_version, env.python_platform.as_ref()))
|
||||
.unwrap_or_default();
|
||||
|
||||
ProgramSettings {
|
||||
python_version: python_version.unwrap_or_default(),
|
||||
python_platform: python_platform.cloned().unwrap_or_default(),
|
||||
search_paths: self.to_search_path_settings(project_root, system),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_search_path_settings(
|
||||
&self,
|
||||
project_root: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> SearchPathSettings {
|
||||
let src_roots =
|
||||
if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_deref()) {
|
||||
vec![src_root.to_path_buf()]
|
||||
} else {
|
||||
let src = project_root.join("src");
|
||||
|
||||
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
|
||||
if system.is_directory(&src) {
|
||||
vec![project_root.to_path_buf(), src]
|
||||
} else {
|
||||
vec![project_root.to_path_buf()]
|
||||
}
|
||||
};
|
||||
|
||||
let (extra_paths, python, typeshed) = self
|
||||
.environment
|
||||
.as_ref()
|
||||
.map(|env| {
|
||||
(
|
||||
env.extra_paths.clone(),
|
||||
env.venv_path.clone(),
|
||||
env.typeshed.clone(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
SearchPathSettings {
|
||||
extra_paths: extra_paths.unwrap_or_default(),
|
||||
src_roots,
|
||||
typeshed,
|
||||
site_packages: python
|
||||
.map(|venv_path| SitePackages::Derived { venv_path })
|
||||
.unwrap_or(SitePackages::Known(vec![])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct EnvironmentOptions {
|
||||
pub python_version: Option<PythonVersion>,
|
||||
|
||||
pub python_platform: Option<PythonPlatform>,
|
||||
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
/// or pyright's stubPath configuration setting.
|
||||
pub extra_paths: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
/// Optional path to a "typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub typeshed: Option<SystemPathBuf>,
|
||||
|
||||
// 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.
|
||||
pub venv_path: Option<SystemPathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SrcOptions {
|
||||
/// The root of the project, used for finding first-party modules.
|
||||
pub root: Option<SystemPathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KnotTomlError {
|
||||
#[error(transparent)]
|
||||
TomlSyntax(#[from] toml::de::Error),
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: root
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("project-root"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: sub_project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("nested-project"),
|
||||
root: "/app/packages/a",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: root
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("project-root"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: Some(EnvironmentOptions(
|
||||
r#python-version: Some("3.10"),
|
||||
r#python-platform: None,
|
||||
r#extra-paths: None,
|
||||
typeshed: None,
|
||||
r#venv-path: None,
|
||||
)),
|
||||
src: None,
|
||||
),
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: sub_project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("nested-project"),
|
||||
root: "/app/packages/a",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: None,
|
||||
),
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: root
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("super-app"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("backend"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: None,
|
||||
),
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("app"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
environment: None,
|
||||
src: None,
|
||||
),
|
||||
)
|
||||
@@ -1,214 +0,0 @@
|
||||
"""A runner for Markdown-based tests for Red Knot"""
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "rich",
|
||||
# "watchfiles",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Final, Literal, Never, assert_never
|
||||
|
||||
from rich.console import Console
|
||||
from watchfiles import Change, watch
|
||||
|
||||
CRATE_NAME: Final = "red_knot_python_semantic"
|
||||
CRATE_ROOT: Final = Path(__file__).resolve().parent
|
||||
MDTEST_DIR: Final = CRATE_ROOT / "resources" / "mdtest"
|
||||
|
||||
|
||||
class MDTestRunner:
|
||||
mdtest_executable: Path | None
|
||||
console: Console
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.mdtest_executable = None
|
||||
self.console = Console()
|
||||
|
||||
def _run_cargo_test(self, *, message_format: Literal["human", "json"]) -> str:
|
||||
return subprocess.check_output(
|
||||
[
|
||||
"cargo",
|
||||
"test",
|
||||
"--package",
|
||||
CRATE_NAME,
|
||||
"--no-run",
|
||||
"--color=always",
|
||||
"--message-format",
|
||||
message_format,
|
||||
],
|
||||
cwd=CRATE_ROOT,
|
||||
env=dict(os.environ, CLI_COLOR="1"),
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
def _recompile_tests(
|
||||
self, status_message: str, *, message_on_success: bool = True
|
||||
) -> bool:
|
||||
with self.console.status(status_message):
|
||||
# Run it with 'human' format in case there are errors:
|
||||
try:
|
||||
self._run_cargo_test(message_format="human")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output)
|
||||
return False
|
||||
|
||||
# Run it again with 'json' format to find the mdtest executable:
|
||||
json_output = self._run_cargo_test(message_format="json")
|
||||
|
||||
if json_output:
|
||||
self._get_executable_path_from_json(json_output)
|
||||
|
||||
if message_on_success:
|
||||
self.console.print("[dim]Tests compiled successfully[/dim]")
|
||||
return True
|
||||
|
||||
def _get_executable_path_from_json(self, json_output: str) -> None:
|
||||
for json_line in json_output.splitlines():
|
||||
try:
|
||||
data = json.loads(json_line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if data.get("target", {}).get("name") == "mdtest":
|
||||
self.mdtest_executable = Path(data["executable"])
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Could not find mdtest executable after successful compilation"
|
||||
)
|
||||
|
||||
def _run_mdtest(
|
||||
self, arguments: list[str] | None = None, *, capture_output: bool = False
|
||||
) -> subprocess.CompletedProcess:
|
||||
assert self.mdtest_executable is not None
|
||||
|
||||
arguments = arguments or []
|
||||
return subprocess.run(
|
||||
[self.mdtest_executable, *arguments],
|
||||
cwd=CRATE_ROOT,
|
||||
env=dict(os.environ, CLICOLOR_FORCE="1"),
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def _run_mdtests_for_file(self, markdown_file: Path) -> None:
|
||||
path_mangled = (
|
||||
markdown_file.as_posix()
|
||||
.replace("/", "_")
|
||||
.replace("-", "_")
|
||||
.removesuffix(".md")
|
||||
)
|
||||
test_name = f"mdtest__{path_mangled}"
|
||||
|
||||
output = self._run_mdtest(["--exact", test_name], capture_output=True)
|
||||
|
||||
if output.returncode == 0:
|
||||
if "running 0 tests\n" in output.stdout:
|
||||
self.console.log(
|
||||
f"[yellow]Warning[/yellow]: No tests were executed with filter '{test_name}'"
|
||||
)
|
||||
else:
|
||||
self.console.print(
|
||||
f"Test for [bold green]{markdown_file}[/bold green] succeeded"
|
||||
)
|
||||
else:
|
||||
self.console.print()
|
||||
self.console.rule(
|
||||
f"Test for [bold red]{markdown_file}[/bold red] failed",
|
||||
style="gray",
|
||||
)
|
||||
self._print_trimmed_cargo_test_output(
|
||||
output.stdout + output.stderr, test_name
|
||||
)
|
||||
|
||||
def _print_trimmed_cargo_test_output(self, output: str, test_name: str) -> None:
|
||||
# Skip 'cargo test' boilerplate at the beginning:
|
||||
lines = output.splitlines()
|
||||
start_index = 0
|
||||
for i, line in enumerate(lines):
|
||||
if f"{test_name} stdout" in line:
|
||||
start_index = i
|
||||
break
|
||||
|
||||
for line in lines[start_index + 1 :]:
|
||||
if "MDTEST_TEST_FILTER" in line:
|
||||
continue
|
||||
if line.strip() == "-" * 50:
|
||||
# Skip 'cargo test' boilerplate at the end
|
||||
break
|
||||
|
||||
print(line)
|
||||
|
||||
def watch(self) -> Never:
|
||||
self._recompile_tests("Compiling tests...", message_on_success=False)
|
||||
self.console.print("[dim]Ready to watch for changes...[/dim]")
|
||||
|
||||
for changes in watch(CRATE_ROOT):
|
||||
new_md_files = set()
|
||||
changed_md_files = set()
|
||||
rust_code_has_changed = False
|
||||
|
||||
for change, path_str in changes:
|
||||
path = Path(path_str)
|
||||
|
||||
if path.suffix == ".rs":
|
||||
rust_code_has_changed = True
|
||||
continue
|
||||
|
||||
if path.suffix != ".md":
|
||||
continue
|
||||
|
||||
try:
|
||||
relative_path = Path(path).relative_to(MDTEST_DIR)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
match change:
|
||||
case Change.added:
|
||||
# When saving a file, some editors (looking at you, Vim) might first
|
||||
# save the file with a temporary name (e.g. `file.md~`) and then rename
|
||||
# it to the final name. This creates a `deleted` and `added` change.
|
||||
# We treat those files as `changed` here.
|
||||
if (Change.deleted, path_str) in changes:
|
||||
changed_md_files.add(relative_path)
|
||||
else:
|
||||
new_md_files.add(relative_path)
|
||||
case Change.modified:
|
||||
changed_md_files.add(relative_path)
|
||||
case Change.deleted:
|
||||
# No need to do anything when a Markdown test is deleted
|
||||
pass
|
||||
case _ as unreachable:
|
||||
assert_never(unreachable)
|
||||
|
||||
if rust_code_has_changed:
|
||||
if self._recompile_tests("Rust code has changed, recompiling tests..."):
|
||||
self._run_mdtest()
|
||||
elif new_md_files:
|
||||
files = " ".join(file.as_posix() for file in new_md_files)
|
||||
self._recompile_tests(
|
||||
f"New Markdown test [yellow]{files}[/yellow] detected, recompiling tests..."
|
||||
)
|
||||
|
||||
for path in new_md_files | changed_md_files:
|
||||
self._run_mdtests_for_file(path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
runner = MDTestRunner()
|
||||
runner.watch()
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,141 +0,0 @@
|
||||
version = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[manifest]
|
||||
requirements = [
|
||||
{ name = "rich" },
|
||||
{ name = "watchfiles" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
|
||||
]
|
||||
@@ -100,7 +100,7 @@ def _(flag: bool):
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
|
||||
baz_1: str = repr(object())
|
||||
baz_1: str = str()
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
|
||||
@@ -173,40 +173,3 @@ p: "call()"
|
||||
r: "[1, 2]"
|
||||
s: "(1, 2)"
|
||||
```
|
||||
|
||||
## Multi line annotation
|
||||
|
||||
Quoted type annotations should be parsed as if surrounded by parentheses.
|
||||
|
||||
```py
|
||||
def valid(
|
||||
a1: """(
|
||||
int |
|
||||
str
|
||||
)
|
||||
""",
|
||||
a2: """
|
||||
int |
|
||||
str
|
||||
""",
|
||||
):
|
||||
reveal_type(a1) # revealed: int | str
|
||||
reveal_type(a2) # revealed: int | str
|
||||
|
||||
def invalid(
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a1: """
|
||||
int |
|
||||
str)
|
||||
""",
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a2: """
|
||||
int) |
|
||||
str
|
||||
""",
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a3: """
|
||||
(int)) """,
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -6,11 +6,14 @@ Several type qualifiers are unsupported by red-knot currently. However, we also
|
||||
false-positive errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly, TypedDict
|
||||
|
||||
X: Final = 42
|
||||
Y: Final[int] = 42
|
||||
|
||||
class Foo:
|
||||
A: ClassVar[int] = 42
|
||||
|
||||
# TODO: `TypedDict` is actually valid as a base
|
||||
# error: [invalid-base]
|
||||
class Bar(TypedDict):
|
||||
|
||||
@@ -2,282 +2,6 @@
|
||||
|
||||
Tests for attribute access on various kinds of types.
|
||||
|
||||
## Class and instance variables
|
||||
|
||||
### Pure instance variables
|
||||
|
||||
#### Variable only declared/bound in `__init__`
|
||||
|
||||
Variables only declared and/or bound in `__init__` are pure instance variables. They cannot be
|
||||
accessed on the class itself.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, value2: int, flag: bool = False) -> None:
|
||||
# bound but not declared
|
||||
self.pure_instance_variable1 = "value set in __init__"
|
||||
|
||||
# bound but not declared - with type inferred from parameter
|
||||
self.pure_instance_variable2 = value2
|
||||
|
||||
# declared but not bound
|
||||
self.pure_instance_variable3: bytes
|
||||
|
||||
# declared and bound
|
||||
self.pure_instance_variable4: bool = True
|
||||
|
||||
# possibly undeclared/unbound
|
||||
if flag:
|
||||
self.pure_instance_variable5: str = "possibly set in __init__"
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
# TODO: should be `Literal["value set in __init__"]`, or `Unknown | Literal[…]` to allow
|
||||
# assignments to this unannotated attribute from other scopes.
|
||||
reveal_type(c_instance.pure_instance_variable1) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `int`
|
||||
reveal_type(c_instance.pure_instance_variable2) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `bytes`
|
||||
reveal_type(c_instance.pure_instance_variable3) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `bool`
|
||||
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should be `str`
|
||||
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
|
||||
# mypy and pyright do not show an error here.
|
||||
reveal_type(c_instance.pure_instance_variable5) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: If we choose to infer a precise `Literal[…]` type for the instance attribute (see
|
||||
# above), this should be an error: incompatible types in assignment. If we choose to infer
|
||||
# a gradual `Unknown | Literal[…]` type, this assignment is fine.
|
||||
c_instance.pure_instance_variable1 = "value set on instance"
|
||||
|
||||
# TODO: this should be an error (incompatible types in assignment)
|
||||
c_instance.pure_instance_variable2 = "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 `pure_instance_variable1`"
|
||||
reveal_type(C.pure_instance_variable1) # 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"
|
||||
C.pure_instance_variable1 = "overwritten on class"
|
||||
|
||||
c_instance.pure_instance_variable4 = False
|
||||
|
||||
# TODO: After this assignment to the attribute within this scope, we may eventually want to narrow
|
||||
# the `bool` type (see above) for this instance variable to `Literal[False]` here. This is unsound
|
||||
# in general (we don't know what else happened to `c_instance` between the assignment and the use
|
||||
# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably
|
||||
# be `Literal[False]`.
|
||||
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
|
||||
```
|
||||
|
||||
#### Variable declared in class body and declared/bound in `__init__`
|
||||
|
||||
The same rule applies even if the variable is *declared* (not bound!) in the class body: it is still
|
||||
a pure instance variable.
|
||||
|
||||
```py
|
||||
class C:
|
||||
pure_instance_variable: str
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.pure_instance_variable = "value set in __init__"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: str
|
||||
|
||||
# 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.pure_instance_variable) # revealed: str
|
||||
|
||||
# TODO: same as above. We plan to emit a diagnostic here, even if both mypy
|
||||
# and pyright allow this.
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
|
||||
# TODO: this should be an error (incompatible types in assignment)
|
||||
c_instance.pure_instance_variable = 1
|
||||
```
|
||||
|
||||
#### Variable only defined in unrelated method
|
||||
|
||||
We also recognize pure instance variables if they are defined in a method that is not `__init__`.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def set_instance_variable(self) -> None:
|
||||
self.pure_instance_variable = "value set in method"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# Not that we would use this in static analysis, but for a more realistic example, let's actually
|
||||
# call the method, so that the attribute is bound if this example is actually run.
|
||||
c_instance.set_instance_variable()
|
||||
|
||||
# TODO: should be `Literal["value set in method"]` or `Unknown | Literal[…]` (see above).
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: We already show an error here, but the message might be improved?
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_instance_variable) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
```
|
||||
|
||||
#### Variable declared in class body and not bound anywhere
|
||||
|
||||
If a variable is declared in the class body but not bound anywhere, we still consider it a pure
|
||||
instance variable and allow access to it via instances.
|
||||
|
||||
```py
|
||||
class C:
|
||||
pure_instance_variable: str
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.pure_instance_variable) # 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.pure_instance_variable) # revealed: str
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit one.
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
```
|
||||
|
||||
### Pure class variables (`ClassVar`)
|
||||
|
||||
#### Annotated with `ClassVar` type qualifier
|
||||
|
||||
Class variables annotated with the [`typing.ClassVar`] type qualifier are pure class variables. They
|
||||
cannot be overwritten on instances, but they can be accessed on instances.
|
||||
|
||||
For more details, see the [typing spec on `ClassVar`].
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
pure_class_variable1: ClassVar[str] = "value in class body"
|
||||
pure_class_variable2: ClassVar = 1
|
||||
|
||||
def method(self):
|
||||
# TODO: this should be an error
|
||||
self.pure_class_variable1 = "value set through instance"
|
||||
|
||||
reveal_type(C.pure_class_variable1) # revealed: str
|
||||
|
||||
# TODO: this should be `Literal[1]`, or `Unknown | Literal[1]`.
|
||||
reveal_type(C.pure_class_variable2) # revealed: Unknown
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# It is okay to access a pure class variable on an instance.
|
||||
reveal_type(c_instance.pure_class_variable1) # revealed: str
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1]`.
|
||||
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown
|
||||
|
||||
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`"
|
||||
c_instance.pure_class_variable1 = "value set on instance"
|
||||
|
||||
C.pure_class_variable1 = "overwritten on class"
|
||||
|
||||
# TODO: should raise an error (incompatible types in assignment)
|
||||
C.pure_class_variable1 = 1
|
||||
|
||||
class Subclass(C):
|
||||
pure_class_variable1: ClassVar[str] = "overwritten on subclass"
|
||||
|
||||
reveal_type(Subclass.pure_class_variable1) # revealed: str
|
||||
```
|
||||
|
||||
#### Variable only mentioned in a class method
|
||||
|
||||
We also consider a class variable to be a pure class variable if it is only mentioned in a class
|
||||
method.
|
||||
|
||||
```py
|
||||
class C:
|
||||
@classmethod
|
||||
def class_method(cls):
|
||||
cls.pure_class_variable = "value set in class method"
|
||||
|
||||
# for a more realistic example, let's actually call the method
|
||||
C.class_method()
|
||||
|
||||
# TODO: We currently plan to support this and show no error here.
|
||||
# mypy shows an error here, pyright does not.
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
|
||||
# TODO: should be `Literal["overwritten on class"]`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
c_instance = C()
|
||||
# TODO: should be `Literal["overwritten on class"]`
|
||||
reveal_type(c_instance.pure_class_variable) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: should raise an error.
|
||||
c_instance.pure_class_variable = "value set on instance"
|
||||
```
|
||||
|
||||
### Instance variables with class-level default values
|
||||
|
||||
These are instance attributes, but the fact that we can see that they have a binding (not a
|
||||
declaration) in the class body means that reading the value from the class directly is also
|
||||
permitted. This is the only difference for these attributes as opposed to "pure" instance
|
||||
attributes.
|
||||
|
||||
#### Basic
|
||||
|
||||
```py
|
||||
class C:
|
||||
variable_with_class_default1: str = "value in class body"
|
||||
variable_with_class_default2 = 1
|
||||
|
||||
def instance_method(self):
|
||||
self.variable_with_class_default1 = "value set in instance method"
|
||||
|
||||
reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
|
||||
# TODO: this should be `Unknown | Literal[1]`.
|
||||
reveal_type(C.variable_with_class_default2) # revealed: Literal[1]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.variable_with_class_default1) # revealed: str
|
||||
reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Literal[1]
|
||||
|
||||
c_instance.variable_with_class_default1 = "value set on instance"
|
||||
|
||||
reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
|
||||
# TODO: Could be Literal["value set on instance"], or still `str` if we choose not to
|
||||
# narrow the type.
|
||||
reveal_type(c_instance.variable_with_class_default1) # revealed: str
|
||||
|
||||
C.variable_with_class_default1 = "overwritten on class"
|
||||
|
||||
# TODO: Could be `Literal["overwritten on class"]`, or still `str` if we choose not to
|
||||
# narrow the type.
|
||||
reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
|
||||
# TODO: should still be `Literal["value set on instance"]`, or `str`.
|
||||
reveal_type(c_instance.variable_with_class_default1) # revealed: str
|
||||
```
|
||||
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
@@ -300,9 +24,7 @@ def _(flag: bool):
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited class attributes
|
||||
|
||||
### Basic
|
||||
## Inherited attributes
|
||||
|
||||
```py
|
||||
class A:
|
||||
@@ -314,7 +36,7 @@ class C(B): ...
|
||||
reveal_type(C.X) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
### Multiple inheritance
|
||||
## Inherited attributes (multiple inheritance)
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
@@ -382,7 +104,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
### Unions with all paths unbound
|
||||
## Unions with all paths unbound
|
||||
|
||||
If the symbol is unbound in all elements of the union, we detect that:
|
||||
|
||||
@@ -436,9 +158,7 @@ class Foo: ...
|
||||
reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Literal types
|
||||
|
||||
### Function-literal attributes
|
||||
## Function-literal attributes
|
||||
|
||||
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
|
||||
functions are instances of that class:
|
||||
@@ -446,8 +166,8 @@ functions are instances of that class:
|
||||
```py path=a.py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
|
||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
reveal_type(f.__defaults__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -459,14 +179,14 @@ reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
|
||||
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
```
|
||||
|
||||
### Int-literal attributes
|
||||
## Int-literal attributes
|
||||
|
||||
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
|
||||
integers are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
reveal_type((2).bit_length) # revealed: @Todo(bound method)
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
reveal_type((2).bit_length) # revealed: @Todo(instance attributes)
|
||||
reveal_type((2).denominator) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -476,14 +196,14 @@ reveal_type((2).numerator) # revealed: Literal[2]
|
||||
reveal_type((2).real) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Bool-literal attributes
|
||||
## Literal `bool` attributes
|
||||
|
||||
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
|
||||
bols are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
reveal_type(True.__and__) # revealed: @Todo(bound method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(bound method)
|
||||
reveal_type(True.__and__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(False.__or__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -493,20 +213,11 @@ reveal_type(True.numerator) # revealed: Literal[1]
|
||||
reveal_type(False.real) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
### Bytes-literal attributes
|
||||
## Bytes-literal attributes
|
||||
|
||||
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
|
||||
|
||||
```py
|
||||
reveal_type(b"foo".join) # revealed: @Todo(bound method)
|
||||
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
|
||||
reveal_type(b"foo".join) # revealed: @Todo(instance attributes)
|
||||
reveal_type(b"foo".endswith) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
[pyright's documentation] on this topic.
|
||||
|
||||
[pyright's documentation]: https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables
|
||||
[typing spec on `classvar`]: https://typing.readthedocs.io/en/latest/spec/class-compat.html#classvar
|
||||
[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
# Boundness and declaredness: public uses
|
||||
|
||||
This document demonstrates how type-inference and diagnostics works for *public* uses of a symbol,
|
||||
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope
|
||||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
|
||||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
|
||||
|
||||
We test the whole matrix of possible boundness and declaredness states. The current behavior is
|
||||
summarized in the following table, while the tests below demonstrate each case. Note that some of
|
||||
this behavior is questionable and might change in the future. See the TODOs in `symbol_by_id`
|
||||
(`types.rs`) and [this issue](https://github.com/astral-sh/ruff/issues/14297) for more information.
|
||||
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
|
||||
"undeclared-and-possibly-unbound" cases (marked with a "?").
|
||||
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ------------ |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
|
||||
| **Diagnostic** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | -------- | ------------------------- | ------------------- |
|
||||
| bound | | | |
|
||||
| possibly-unbound | | `possibly-unbound-import` | ? |
|
||||
| unbound | | ? | `unresolved-import` |
|
||||
|
||||
## Declared
|
||||
|
||||
### Declared and bound
|
||||
|
||||
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
|
||||
(`Literal[1]`), or a conflicting inferred type (`Literal[2]`):
|
||||
|
||||
```py path=mod.py
|
||||
x: int = 1
|
||||
|
||||
# error: [invalid-assignment]
|
||||
y: str = 2
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: str
|
||||
```
|
||||
|
||||
### Declared and possibly unbound
|
||||
|
||||
If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type
|
||||
without raising an error.
|
||||
|
||||
```py path=mod.py
|
||||
def flag() -> bool: ...
|
||||
|
||||
x: int
|
||||
y: str
|
||||
if flag:
|
||||
x = 1
|
||||
# error: [invalid-assignment]
|
||||
y = 2
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: str
|
||||
```
|
||||
|
||||
### Declared and unbound
|
||||
|
||||
Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol
|
||||
is available somehow and simply use the declared type.
|
||||
|
||||
```py path=mod.py
|
||||
x: int
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly undeclared
|
||||
|
||||
### Possibly undeclared and bound
|
||||
|
||||
If a symbol is possibly undeclared but definitely bound, we use the union of the declared and
|
||||
inferred types:
|
||||
|
||||
```py path=mod.py
|
||||
from typing import Any
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
x = 1
|
||||
y = 2
|
||||
if flag():
|
||||
x: Any
|
||||
# error: [invalid-declaration]
|
||||
y: str
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | Unknown
|
||||
```
|
||||
|
||||
### Possibly undeclared and possibly unbound
|
||||
|
||||
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
|
||||
inferred types. This case is interesting because the "possibly declared" definition might not be the
|
||||
same as the "possibly bound" definition (symbol `y`). Note that we raise a `possibly-unbound-import`
|
||||
error for both `x` and `y`:
|
||||
|
||||
```py path=mod.py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
x: Any = 1
|
||||
y = 2
|
||||
else:
|
||||
y: str
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import]
|
||||
# error: [possibly-unbound-import]
|
||||
from mod import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | str
|
||||
```
|
||||
|
||||
### Possibly undeclared and unbound
|
||||
|
||||
If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This
|
||||
seems inconsistent when compared to the case just above.
|
||||
|
||||
```py path=mod.py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
x: int
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Undeclared
|
||||
|
||||
### Undeclared but bound
|
||||
|
||||
We use the inferred type as the public type, if a symbol has no declared type.
|
||||
|
||||
```py path=mod.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
|
||||
If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems
|
||||
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
|
||||
|
||||
```py path=mod.py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag:
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
|
||||
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
|
||||
|
||||
```py path=mod.py
|
||||
if False:
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
@@ -169,15 +169,6 @@ def f(*args: int) -> int:
|
||||
reveal_type(f(1, 2, 3)) # revealed: int
|
||||
```
|
||||
|
||||
### Multiple keyword arguments map to keyword variadic parameter
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(foo=1, bar=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Missing arguments
|
||||
|
||||
### No defaults or variadic
|
||||
|
||||
@@ -92,7 +92,8 @@ def _(o: object):
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: ~None
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -110,8 +110,10 @@ python-version = "3.10"
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(a: str | int):
|
||||
assert_type(a, str | int)
|
||||
assert_type(a, int | str)
|
||||
assert_type(a, str | int) # fine
|
||||
|
||||
# TODO: Order-independent union handling in type equivalence
|
||||
assert_type(a, int | str) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Intersections
|
||||
@@ -133,6 +135,8 @@ def _(a: A):
|
||||
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
|
||||
reveal_type(a) # revealed: A & B & ~C & ~D
|
||||
|
||||
assert_type(a, Intersection[A, B, Not[C], Not[D]])
|
||||
assert_type(a, Intersection[B, A, Not[D], Not[C]])
|
||||
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
|
||||
|
||||
# TODO: Order-independent intersection handling in type equivalence
|
||||
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# `cast`
|
||||
|
||||
`cast()` takes two arguments, one type and one value, and returns a value of the given type.
|
||||
|
||||
The (inferred) type of the value and the given type do not need to have any correlation.
|
||||
|
||||
```py
|
||||
from typing import Literal, cast
|
||||
|
||||
reveal_type(True) # revealed: Literal[True]
|
||||
reveal_type(cast(str, True)) # revealed: str
|
||||
reveal_type(cast("str", True)) # revealed: str
|
||||
|
||||
reveal_type(cast(int | str, 1)) # revealed: int | str
|
||||
|
||||
# error: [invalid-type-form]
|
||||
reveal_type(cast(Literal, True)) # revealed: Unknown
|
||||
|
||||
# TODO: These should be errors
|
||||
cast(1)
|
||||
cast(str)
|
||||
cast(str, b"ar", "foo")
|
||||
|
||||
# TODO: Either support keyword arguments properly,
|
||||
# or give a comprehensible error message saying they're unsupported
|
||||
cast(val="foo", typ=int) # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
```
|
||||
@@ -17,8 +17,8 @@ box: MyBox[int] = MyBox(5)
|
||||
# TODO should emit a diagnostic here (str is not assignable to int)
|
||||
wrong_innards: MyBox[int] = MyBox("five")
|
||||
|
||||
# TODO reveal int, do not leak the typevar
|
||||
reveal_type(box.data) # revealed: T
|
||||
# TODO reveal int
|
||||
reveal_type(box.data) # revealed: @Todo(instance attributes)
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
```
|
||||
@@ -39,9 +39,7 @@ class MySecureBox[T](MyBox[T]): ...
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
# TODO reveal int
|
||||
# The @Todo(…) is misleading here. We currently treat `MyBox[T]` as a dynamic base class because we
|
||||
# don't understand generics and therefore infer `Unknown` for the `MyBox[T]` base of `MySecureBox[T]`.
|
||||
reveal_type(secure_box.data) # revealed: @Todo(instance attribute on class with dynamic base)
|
||||
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Cyclical class definition
|
||||
|
||||
@@ -283,21 +283,6 @@ def _(
|
||||
reveal_type(not_object) # revealed: Never
|
||||
```
|
||||
|
||||
### `object & ~T` is equivalent to `~T`
|
||||
|
||||
A second consequence of the fact that `object` is the top type is that `object` is always redundant
|
||||
in intersections, and can be eagerly simplified out. `object & P` is equivalent to `P`;
|
||||
`object & ~P` is equivalent to `~P` for any type `P`.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, is_equivalent_to, static_assert
|
||||
|
||||
class P: ...
|
||||
|
||||
static_assert(is_equivalent_to(Intersection[object, P], P))
|
||||
static_assert(is_equivalent_to(Intersection[object, Not[P]], Not[P]))
|
||||
```
|
||||
|
||||
### Intersection of a type and its negation
|
||||
|
||||
Continuing with more [complement laws], if we see both `P` and `~P` in an intersection, we can
|
||||
@@ -650,91 +635,6 @@ def _(
|
||||
reveal_type(i8) # revealed: Never
|
||||
```
|
||||
|
||||
### Simplifications of `bool`, `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
In general, intersections with `AlwaysTruthy` and `AlwaysFalsy` cannot be simplified. Naively, you
|
||||
might think that `int & AlwaysFalsy` could simplify to `Literal[0]`, but this is not the case: for
|
||||
example, the `False` constant inhabits the type `int & AlwaysFalsy` (due to the fact that
|
||||
`False.__class__` is `bool` at runtime, and `bool` subclasses `int`), but `False` does not inhabit
|
||||
the type `Literal[0]`.
|
||||
|
||||
Nonetheless, intersections of `AlwaysFalsy` or `AlwaysTruthy` with `bool` _can_ be simplified, due
|
||||
to the fact that `bool` is a `@final` class at runtime that cannot be subclassed.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy
|
||||
|
||||
class P: ...
|
||||
|
||||
def f(
|
||||
a: Intersection[bool, AlwaysTruthy],
|
||||
b: Intersection[bool, AlwaysFalsy],
|
||||
c: Intersection[bool, Not[AlwaysTruthy]],
|
||||
d: Intersection[bool, Not[AlwaysFalsy]],
|
||||
e: Intersection[bool, AlwaysTruthy, P],
|
||||
f: Intersection[bool, AlwaysFalsy, P],
|
||||
g: Intersection[bool, Not[AlwaysTruthy], P],
|
||||
h: Intersection[bool, Not[AlwaysFalsy], P],
|
||||
):
|
||||
reveal_type(a) # revealed: Literal[True]
|
||||
reveal_type(b) # revealed: Literal[False]
|
||||
reveal_type(c) # revealed: Literal[False]
|
||||
reveal_type(d) # revealed: Literal[True]
|
||||
|
||||
# `bool & AlwaysTruthy & P` -> `Literal[True] & P` -> `Never`
|
||||
reveal_type(e) # revealed: Never
|
||||
reveal_type(f) # revealed: Never
|
||||
reveal_type(g) # revealed: Never
|
||||
reveal_type(h) # revealed: Never
|
||||
```
|
||||
|
||||
## Simplification of `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
Similarly, intersections between `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` can be
|
||||
simplified, due to the fact that a `LiteralString` inhabitant is known to have `__class__` set to
|
||||
exactly `str` (and not a subclass of `str`):
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy, Unknown
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
def f(
|
||||
a: Intersection[LiteralString, AlwaysTruthy],
|
||||
b: Intersection[LiteralString, AlwaysFalsy],
|
||||
c: Intersection[LiteralString, Not[AlwaysTruthy]],
|
||||
d: Intersection[LiteralString, Not[AlwaysFalsy]],
|
||||
e: Intersection[AlwaysFalsy, LiteralString],
|
||||
f: Intersection[Not[AlwaysTruthy], LiteralString],
|
||||
g: Intersection[AlwaysTruthy, LiteralString],
|
||||
h: Intersection[Not[AlwaysFalsy], LiteralString],
|
||||
i: Intersection[Unknown, LiteralString, AlwaysFalsy],
|
||||
j: Intersection[Not[AlwaysTruthy], Unknown, LiteralString],
|
||||
):
|
||||
reveal_type(a) # revealed: LiteralString & ~Literal[""]
|
||||
reveal_type(b) # revealed: Literal[""]
|
||||
reveal_type(c) # revealed: Literal[""]
|
||||
reveal_type(d) # revealed: LiteralString & ~Literal[""]
|
||||
reveal_type(e) # revealed: Literal[""]
|
||||
reveal_type(f) # revealed: Literal[""]
|
||||
reveal_type(g) # revealed: LiteralString & ~Literal[""]
|
||||
reveal_type(h) # revealed: LiteralString & ~Literal[""]
|
||||
reveal_type(i) # revealed: Unknown & Literal[""]
|
||||
reveal_type(j) # revealed: Unknown & Literal[""]
|
||||
```
|
||||
|
||||
## Addition of a type to an intersection with many non-disjoint types
|
||||
|
||||
This slightly strange-looking test is a regression test for a mistake that was nearly made in a PR:
|
||||
<https://github.com/astral-sh/ruff/pull/15475#discussion_r1915041987>.
|
||||
|
||||
```py
|
||||
from knot_extensions import AlwaysFalsy, Intersection, Unknown
|
||||
from typing_extensions import Literal
|
||||
|
||||
def _(x: Intersection[str, Unknown, AlwaysFalsy, Literal[""]]):
|
||||
reveal_type(x) # revealed: Unknown & Literal[""]
|
||||
```
|
||||
|
||||
## Non fully-static types
|
||||
|
||||
### Negation of dynamic types
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
## Expression
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
x = 0
|
||||
y = str()
|
||||
z = False
|
||||
|
||||
def _(x: Literal[0], y: str, z: Literal[False]):
|
||||
reveal_type(f"hello") # revealed: Literal["hello"]
|
||||
reveal_type(f"h {x}") # revealed: Literal["h 0"]
|
||||
reveal_type("one " f"single " f"literal") # revealed: Literal["one single literal"]
|
||||
reveal_type("first " f"second({x})" f" third") # revealed: Literal["first second(0) third"]
|
||||
reveal_type(f"-{y}-") # revealed: str
|
||||
reveal_type(f"-{y}-" f"--" "--") # revealed: str
|
||||
reveal_type(f"{z} == {False} is {True}") # revealed: Literal["False == False is True"]
|
||||
reveal_type(f"hello") # revealed: Literal["hello"]
|
||||
reveal_type(f"h {x}") # revealed: Literal["h 0"]
|
||||
reveal_type("one " f"single " f"literal") # revealed: Literal["one single literal"]
|
||||
reveal_type("first " f"second({x})" f" third") # revealed: Literal["first second(0) third"]
|
||||
reveal_type(f"-{y}-") # revealed: str
|
||||
reveal_type(f"-{y}-" f"--" "--") # revealed: str
|
||||
reveal_type(f"{z} == {False} is {True}") # revealed: Literal["False == False is True"]
|
||||
```
|
||||
|
||||
## Conversion Flags
|
||||
|
||||
@@ -396,10 +396,11 @@ class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# Avoid emitting the errors for these. The classes have cyclic superclasses,
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ...
|
||||
class Spam(Baz): ...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
|
||||
@@ -91,7 +91,8 @@ if isinstance(x, (A, B)):
|
||||
elif isinstance(x, (A, C)):
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
else:
|
||||
reveal_type(x) # revealed: ~A & ~B & ~C
|
||||
# TODO: Should be simplified to ~A & ~B & ~C
|
||||
reveal_type(x) # revealed: object & ~A & ~B & ~C
|
||||
```
|
||||
|
||||
## No narrowing for instances of `builtins.type`
|
||||
@@ -180,39 +181,3 @@ def _(x: object, y: type[int]):
|
||||
if isinstance(x, y):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Adding a disjoint element to an existing intersection
|
||||
|
||||
We used to incorrectly infer `Literal` booleans for some of these.
|
||||
|
||||
```py
|
||||
from knot_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy
|
||||
|
||||
class P: ...
|
||||
|
||||
def f(
|
||||
a: Intersection[P, AlwaysTruthy],
|
||||
b: Intersection[P, AlwaysFalsy],
|
||||
c: Intersection[P, Not[AlwaysTruthy]],
|
||||
d: Intersection[P, Not[AlwaysFalsy]],
|
||||
):
|
||||
if isinstance(a, bool):
|
||||
reveal_type(a) # revealed: Never
|
||||
else:
|
||||
reveal_type(a) # revealed: P & AlwaysTruthy
|
||||
|
||||
if isinstance(b, bool):
|
||||
reveal_type(b) # revealed: Never
|
||||
else:
|
||||
reveal_type(b) # revealed: P & AlwaysFalsy
|
||||
|
||||
if isinstance(c, bool):
|
||||
reveal_type(c) # revealed: Never
|
||||
else:
|
||||
reveal_type(c) # revealed: P & ~AlwaysTruthy
|
||||
|
||||
if isinstance(d, bool):
|
||||
reveal_type(d) # revealed: Never
|
||||
else:
|
||||
reveal_type(d) # revealed: P & ~AlwaysFalsy
|
||||
```
|
||||
|
||||
@@ -246,31 +246,3 @@ def _(x: type, y: type[int]):
|
||||
if issubclass(x, y):
|
||||
reveal_type(x) # revealed: type[int]
|
||||
```
|
||||
|
||||
### Disjoint `type[]` types are narrowed to `Never`
|
||||
|
||||
Here, `type[UsesMeta1]` and `type[UsesMeta2]` are disjoint because a common subclass of `UsesMeta1`
|
||||
and `UsesMeta2` could only exist if a common subclass of their metaclasses could exist. This is
|
||||
known to be impossible due to the fact that `Meta1` is marked as `@final`.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class Meta1(type): ...
|
||||
|
||||
class Meta2(type): ...
|
||||
class UsesMeta1(metaclass=Meta1): ...
|
||||
class UsesMeta2(metaclass=Meta2): ...
|
||||
|
||||
def _(x: type[UsesMeta1], y: type[UsesMeta2]):
|
||||
if issubclass(x, y):
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: type[UsesMeta1]
|
||||
|
||||
if issubclass(y, x):
|
||||
reveal_type(y) # revealed: Never
|
||||
else:
|
||||
reveal_type(y) # revealed: type[UsesMeta2]
|
||||
```
|
||||
|
||||
@@ -199,7 +199,7 @@ def f(x: Literal[0, 1], y: Literal["", "hello"]):
|
||||
reveal_type(y) # revealed: Literal["", "hello"]
|
||||
```
|
||||
|
||||
## Control Flow Merging
|
||||
## ControlFlow Merging
|
||||
|
||||
After merging control flows, when we take the union of all constraints applied in each branch, we
|
||||
should return to the original state.
|
||||
@@ -312,20 +312,3 @@ def _(x: type[FalsyClass] | type[TruthyClass]):
|
||||
reveal_type(x or A()) # revealed: type[TruthyClass] | A
|
||||
reveal_type(x and A()) # revealed: type[FalsyClass] | A
|
||||
```
|
||||
|
||||
## Truthiness narrowing for `LiteralString`
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
def _(x: LiteralString):
|
||||
if x:
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal[""]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[""]
|
||||
|
||||
if not x:
|
||||
reveal_type(x) # revealed: Literal[""]
|
||||
else:
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal[""]
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ type IntOrStr = int | str
|
||||
# TODO: This should either fall back to the specified type from typeshed,
|
||||
# which is `Any`, or be the actual type of the runtime value expression
|
||||
# `int | str`, i.e. `types.UnionType`.
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(@property)
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
@@ -74,22 +74,5 @@ type ListOrSet[T] = list[T] | set[T]
|
||||
|
||||
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
|
||||
# as specified in the `typeshed` stubs.
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
## `TypeAliasType` properties
|
||||
|
||||
Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_equivalent_to, is_disjoint_from, TypeOf
|
||||
|
||||
type Alias1 = int
|
||||
type Alias2 = int
|
||||
|
||||
type TypeAliasType1 = TypeOf[Alias1]
|
||||
type TypeAliasType2 = TypeOf[Alias2]
|
||||
|
||||
static_assert(not is_equivalent_to(TypeAliasType1, TypeAliasType2))
|
||||
static_assert(is_disjoint_from(TypeAliasType1, TypeAliasType2))
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
@@ -63,7 +63,7 @@ reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(@property)
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
@@ -95,8 +95,8 @@ from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties, and generics;
|
||||
# should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(@property)
|
||||
reveal_type(foo_dict) # revealed: @Todo(@property)
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
@@ -13,7 +13,7 @@ typeshed:
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform) # revealed: LiteralString
|
||||
reveal_type(sys.platform) # revealed: str
|
||||
```
|
||||
|
||||
## Explicit selection of `all` platforms
|
||||
@@ -26,7 +26,7 @@ python-platform = "all"
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform) # revealed: LiteralString
|
||||
reveal_type(sys.platform) # revealed: str
|
||||
```
|
||||
|
||||
## Explicit selection of a specific platform
|
||||
@@ -66,6 +66,6 @@ It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(Attribute access on `LiteralString` types)
|
||||
reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(Attribute access on `LiteralString` types)
|
||||
reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
@@ -117,9 +117,9 @@ properties on instance types:
|
||||
```py path=b.py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Accessing fields by index/slice
|
||||
|
||||
@@ -94,36 +94,6 @@ reveal_type(C.__mro__)
|
||||
u: Unknown[str]
|
||||
```
|
||||
|
||||
### `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
`AlwaysTruthy` and `AlwaysFalsy` represent the sets of all possible objects whose truthiness is
|
||||
always truthy or falsy, respectively.
|
||||
|
||||
They do not accept any type arguments.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(Literal[True], AlwaysTruthy))
|
||||
static_assert(is_subtype_of(Literal[False], AlwaysFalsy))
|
||||
|
||||
static_assert(not is_subtype_of(int, AlwaysFalsy))
|
||||
static_assert(not is_subtype_of(str, AlwaysFalsy))
|
||||
|
||||
def _(t: AlwaysTruthy, f: AlwaysFalsy):
|
||||
reveal_type(t) # revealed: AlwaysTruthy
|
||||
reveal_type(f) # revealed: AlwaysFalsy
|
||||
|
||||
def f(
|
||||
a: AlwaysTruthy[int], # error: [invalid-type-form]
|
||||
b: AlwaysFalsy[str], # error: [invalid-type-form]
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Static assertions
|
||||
|
||||
### Basics
|
||||
|
||||
@@ -33,7 +33,7 @@ in strict mode.
|
||||
```py
|
||||
def f(x: type):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(bound method)
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
@@ -48,7 +48,7 @@ x: type = A() # error: [invalid-assignment]
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(bound method)
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
# Disjointness relation
|
||||
|
||||
Two types `S` and `T` are disjoint if their intersection `S & T` is empty (equivalent to `Never`).
|
||||
This means that it is known that no possible runtime object inhabits both types simultaneously.
|
||||
|
||||
## Basic builtin types
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString, Any
|
||||
from knot_extensions import Intersection, Not, TypeOf, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(bool, str))
|
||||
static_assert(not is_disjoint_from(bool, bool))
|
||||
static_assert(not is_disjoint_from(bool, int))
|
||||
static_assert(not is_disjoint_from(bool, object))
|
||||
|
||||
static_assert(not is_disjoint_from(Any, bool))
|
||||
static_assert(not is_disjoint_from(Any, Any))
|
||||
|
||||
static_assert(not is_disjoint_from(LiteralString, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, LiteralString))
|
||||
static_assert(not is_disjoint_from(str, type))
|
||||
static_assert(not is_disjoint_from(str, type[Any]))
|
||||
```
|
||||
|
||||
## Class hierarchies
|
||||
|
||||
```py
|
||||
from knot_extensions import is_disjoint_from, static_assert, Intersection, is_subtype_of
|
||||
from typing import final
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
|
||||
# B1 and B2 are subclasses of A, so they are not disjoint from A:
|
||||
static_assert(not is_disjoint_from(A, B1))
|
||||
static_assert(not is_disjoint_from(A, B2))
|
||||
|
||||
# The two subclasses B1 and B2 are also not disjoint ...
|
||||
static_assert(not is_disjoint_from(B1, B2))
|
||||
|
||||
# ... because they could share a common subclass ...
|
||||
class C(B1, B2): ...
|
||||
|
||||
# ... which lies in their intersection:
|
||||
static_assert(is_subtype_of(C, Intersection[B1, B2]))
|
||||
|
||||
# However, if a class is marked final, it can not be subclassed ...
|
||||
@final
|
||||
class FinalSubclass(A): ...
|
||||
|
||||
static_assert(not is_disjoint_from(FinalSubclass, A))
|
||||
|
||||
# ... which makes it disjoint from B1, B2:
|
||||
static_assert(is_disjoint_from(B1, FinalSubclass))
|
||||
static_assert(is_disjoint_from(B2, FinalSubclass))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(tuple[()], TypeOf[object]))
|
||||
static_assert(is_disjoint_from(tuple[()], TypeOf[Literal]))
|
||||
|
||||
static_assert(is_disjoint_from(tuple[None], None))
|
||||
static_assert(is_disjoint_from(tuple[None], Literal[b"a"]))
|
||||
static_assert(is_disjoint_from(tuple[None], Literal["a"]))
|
||||
static_assert(is_disjoint_from(tuple[None], Literal[1]))
|
||||
static_assert(is_disjoint_from(tuple[None], Literal[True]))
|
||||
|
||||
static_assert(is_disjoint_from(tuple[Literal[1]], tuple[Literal[2]]))
|
||||
static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1]]))
|
||||
static_assert(is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], Literal[3]]))
|
||||
|
||||
static_assert(not is_disjoint_from(tuple[Literal[1], Literal[2]], tuple[Literal[1], int]))
|
||||
```
|
||||
|
||||
## Unions
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import Intersection, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(Literal[1, 2], Literal[3]))
|
||||
static_assert(is_disjoint_from(Literal[1, 2], Literal[3, 4]))
|
||||
|
||||
static_assert(not is_disjoint_from(Literal[1, 2], Literal[2]))
|
||||
static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3]))
|
||||
```
|
||||
|
||||
## Intersections
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, final
|
||||
from knot_extensions import Intersection, is_disjoint_from, static_assert
|
||||
|
||||
@final
|
||||
class P: ...
|
||||
|
||||
@final
|
||||
class Q: ...
|
||||
|
||||
@final
|
||||
class R: ...
|
||||
|
||||
# For three pairwise disjoint classes ...
|
||||
static_assert(is_disjoint_from(P, Q))
|
||||
static_assert(is_disjoint_from(P, R))
|
||||
static_assert(is_disjoint_from(Q, R))
|
||||
|
||||
# ... their intersections are also disjoint:
|
||||
static_assert(is_disjoint_from(Intersection[P, Q], R))
|
||||
static_assert(is_disjoint_from(Intersection[P, R], Q))
|
||||
static_assert(is_disjoint_from(Intersection[Q, R], P))
|
||||
|
||||
# On the other hand, for non-disjoint classes ...
|
||||
class X: ...
|
||||
class Y: ...
|
||||
class Z: ...
|
||||
|
||||
static_assert(not is_disjoint_from(X, Y))
|
||||
static_assert(not is_disjoint_from(X, Z))
|
||||
static_assert(not is_disjoint_from(Y, Z))
|
||||
|
||||
# ... their intersections are also not disjoint:
|
||||
static_assert(not is_disjoint_from(Intersection[X, Y], Z))
|
||||
static_assert(not is_disjoint_from(Intersection[X, Z], Y))
|
||||
static_assert(not is_disjoint_from(Intersection[Y, Z], X))
|
||||
```
|
||||
|
||||
## Special types
|
||||
|
||||
### `Never`
|
||||
|
||||
`Never` is disjoint from every type, including itself.
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(Never, Never))
|
||||
static_assert(is_disjoint_from(Never, None))
|
||||
static_assert(is_disjoint_from(Never, int))
|
||||
static_assert(is_disjoint_from(Never, object))
|
||||
```
|
||||
|
||||
### `None`
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(None, Literal[True]))
|
||||
static_assert(is_disjoint_from(None, Literal[1]))
|
||||
static_assert(is_disjoint_from(None, Literal["test"]))
|
||||
static_assert(is_disjoint_from(None, Literal[b"test"]))
|
||||
static_assert(is_disjoint_from(None, LiteralString))
|
||||
static_assert(is_disjoint_from(None, int))
|
||||
static_assert(is_disjoint_from(None, type[object]))
|
||||
|
||||
static_assert(not is_disjoint_from(None, None))
|
||||
static_assert(not is_disjoint_from(None, int | None))
|
||||
static_assert(not is_disjoint_from(None, object))
|
||||
```
|
||||
|
||||
### Literals
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(Literal[True], Literal[False]))
|
||||
static_assert(is_disjoint_from(Literal[True], Literal[1]))
|
||||
static_assert(is_disjoint_from(Literal[False], Literal[0]))
|
||||
|
||||
static_assert(is_disjoint_from(Literal[1], Literal[2]))
|
||||
|
||||
static_assert(is_disjoint_from(Literal["a"], Literal["b"]))
|
||||
|
||||
static_assert(is_disjoint_from(Literal[b"a"], LiteralString))
|
||||
static_assert(is_disjoint_from(Literal[b"a"], Literal[b"b"]))
|
||||
static_assert(is_disjoint_from(Literal[b"a"], Literal["a"]))
|
||||
|
||||
static_assert(is_disjoint_from(type[object], TypeOf[Literal]))
|
||||
static_assert(is_disjoint_from(type[str], LiteralString))
|
||||
|
||||
static_assert(not is_disjoint_from(Literal[True], Literal[True]))
|
||||
static_assert(not is_disjoint_from(Literal[False], Literal[False]))
|
||||
static_assert(not is_disjoint_from(Literal[True], bool))
|
||||
static_assert(not is_disjoint_from(Literal[True], int))
|
||||
|
||||
static_assert(not is_disjoint_from(Literal[1], Literal[1]))
|
||||
|
||||
static_assert(not is_disjoint_from(Literal["a"], Literal["a"]))
|
||||
static_assert(not is_disjoint_from(Literal["a"], LiteralString))
|
||||
static_assert(not is_disjoint_from(Literal["a"], str))
|
||||
```
|
||||
|
||||
### Class, module and function literals
|
||||
|
||||
```py
|
||||
from types import ModuleType, FunctionType
|
||||
from knot_extensions import TypeOf, is_disjoint_from, static_assert
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
type LiteralA = TypeOf[A]
|
||||
type LiteralB = TypeOf[B]
|
||||
|
||||
# Class literals for different classes are always disjoint.
|
||||
# They are singleton types that only contain the class object itself.
|
||||
static_assert(is_disjoint_from(LiteralA, LiteralB))
|
||||
|
||||
# The class A is a subclass of A, so A is not disjoint from type[A]:
|
||||
static_assert(not is_disjoint_from(LiteralA, type[A]))
|
||||
|
||||
# The class A is disjoint from type[B] because it's not a subclass of B:
|
||||
static_assert(is_disjoint_from(LiteralA, type[B]))
|
||||
|
||||
# However, type[A] is not disjoint from type[B], as there could be
|
||||
# classes that inherit from both A and B:
|
||||
static_assert(not is_disjoint_from(type[A], type[B]))
|
||||
|
||||
import random
|
||||
import math
|
||||
|
||||
static_assert(is_disjoint_from(TypeOf[random], TypeOf[math]))
|
||||
static_assert(not is_disjoint_from(TypeOf[random], ModuleType))
|
||||
static_assert(not is_disjoint_from(TypeOf[random], object))
|
||||
|
||||
def f(): ...
|
||||
def g(): ...
|
||||
|
||||
static_assert(is_disjoint_from(TypeOf[f], TypeOf[g]))
|
||||
static_assert(not is_disjoint_from(TypeOf[f], FunctionType))
|
||||
static_assert(not is_disjoint_from(TypeOf[f], object))
|
||||
```
|
||||
|
||||
### `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
```py
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(None, AlwaysTruthy))
|
||||
static_assert(not is_disjoint_from(None, AlwaysFalsy))
|
||||
|
||||
static_assert(is_disjoint_from(AlwaysFalsy, AlwaysTruthy))
|
||||
static_assert(not is_disjoint_from(str, AlwaysFalsy))
|
||||
static_assert(not is_disjoint_from(str, AlwaysTruthy))
|
||||
|
||||
static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy))
|
||||
static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy))
|
||||
```
|
||||
|
||||
### Instance types versus `type[T]` types
|
||||
|
||||
An instance type is disjoint from a `type[T]` type if the instance type is `@final` and the class of
|
||||
the instance type is not a subclass of `T`'s metaclass.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
|
||||
@final
|
||||
class Foo: ...
|
||||
|
||||
static_assert(is_disjoint_from(Foo, type[int]))
|
||||
static_assert(is_disjoint_from(type[object], Foo))
|
||||
static_assert(is_disjoint_from(type[dict], Foo))
|
||||
|
||||
# Instance types can be disjoint from `type[]` types
|
||||
# even if the instance type is a subtype of `type`
|
||||
|
||||
@final
|
||||
class Meta1(type): ...
|
||||
|
||||
class UsesMeta1(metaclass=Meta1): ...
|
||||
|
||||
static_assert(not is_disjoint_from(Meta1, type[UsesMeta1]))
|
||||
|
||||
class Meta2(type): ...
|
||||
class UsesMeta2(metaclass=Meta2): ...
|
||||
|
||||
static_assert(not is_disjoint_from(Meta2, type[UsesMeta2]))
|
||||
static_assert(is_disjoint_from(Meta1, type[UsesMeta2]))
|
||||
```
|
||||
|
||||
### `type[T]` versus `type[S]`
|
||||
|
||||
By the same token, `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the
|
||||
metaclass of `S`.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from knot_extensions import static_assert, is_disjoint_from
|
||||
|
||||
@final
|
||||
class Meta1(type): ...
|
||||
|
||||
class Meta2(type): ...
|
||||
class UsesMeta1(metaclass=Meta1): ...
|
||||
class UsesMeta2(metaclass=Meta2): ...
|
||||
|
||||
static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2]))
|
||||
```
|
||||
@@ -1,87 +0,0 @@
|
||||
# Equivalence relation
|
||||
|
||||
`is_equivalent_to` implements [the equivalence relation] for fully static types.
|
||||
|
||||
Two types `A` and `B` are equivalent iff `A` is a subtype of `B` and `B` is a subtype of `A`.
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import Unknown, is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(Literal[1, 2], Literal[1, 2]))
|
||||
static_assert(is_equivalent_to(type[object], type))
|
||||
|
||||
static_assert(not is_equivalent_to(Any, Any))
|
||||
static_assert(not is_equivalent_to(Unknown, Unknown))
|
||||
static_assert(not is_equivalent_to(Any, None))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 0]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2], Literal[1, 2, 3]))
|
||||
```
|
||||
|
||||
## Equivalence is commutative
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
from knot_extensions import is_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_equivalent_to(type, type[object]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 0], Literal[1, 2]))
|
||||
static_assert(not is_equivalent_to(Literal[1, 2, 3], Literal[1, 2]))
|
||||
```
|
||||
|
||||
## Differently ordered intersections and unions are equivalent
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert, Intersection, Not
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
class S: ...
|
||||
|
||||
static_assert(is_equivalent_to(P | Q | R, P | R | Q)) # 1
|
||||
static_assert(is_equivalent_to(P | Q | R, Q | P | R)) # 2
|
||||
static_assert(is_equivalent_to(P | Q | R, Q | R | P)) # 3
|
||||
static_assert(is_equivalent_to(P | Q | R, R | P | Q)) # 4
|
||||
static_assert(is_equivalent_to(P | Q | R, R | Q | P)) # 5
|
||||
static_assert(is_equivalent_to(P | R | Q, Q | P | R)) # 6
|
||||
static_assert(is_equivalent_to(P | R | Q, Q | R | P)) # 7
|
||||
static_assert(is_equivalent_to(P | R | Q, R | P | Q)) # 8
|
||||
static_assert(is_equivalent_to(P | R | Q, R | Q | P)) # 9
|
||||
static_assert(is_equivalent_to(Q | P | R, Q | R | P)) # 10
|
||||
static_assert(is_equivalent_to(Q | P | R, R | P | Q)) # 11
|
||||
static_assert(is_equivalent_to(Q | P | R, R | Q | P)) # 12
|
||||
static_assert(is_equivalent_to(Q | R | P, R | P | Q)) # 13
|
||||
static_assert(is_equivalent_to(Q | R | P, R | Q | P)) # 14
|
||||
static_assert(is_equivalent_to(R | P | Q, R | Q | P)) # 15
|
||||
|
||||
static_assert(is_equivalent_to(str | None, None | str))
|
||||
|
||||
static_assert(is_equivalent_to(Intersection[P, Q], Intersection[Q, P]))
|
||||
static_assert(is_equivalent_to(Intersection[Q, Not[P]], Intersection[Not[P], Q]))
|
||||
static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P], R, Q]))
|
||||
static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q]))
|
||||
```
|
||||
|
||||
## Tuples containing equivalent but differently ordered unions/intersections are equivalent
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not
|
||||
from typing import Literal
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
class S: ...
|
||||
|
||||
static_assert(is_equivalent_to(tuple[P | Q], tuple[Q | P]))
|
||||
static_assert(is_equivalent_to(tuple[P | None], tuple[None | P]))
|
||||
static_assert(
|
||||
is_equivalent_to(tuple[Intersection[P, Q] | Intersection[R, Not[S]]], tuple[Intersection[Not[S], R] | Intersection[Q, P]])
|
||||
)
|
||||
```
|
||||
|
||||
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
|
||||
@@ -1,54 +0,0 @@
|
||||
# Fully-static types
|
||||
|
||||
A type is fully static iff it does not contain any gradual forms.
|
||||
|
||||
## Fully-static
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString, Never
|
||||
from knot_extensions import Intersection, Not, TypeOf, is_fully_static, static_assert
|
||||
|
||||
static_assert(is_fully_static(Never))
|
||||
static_assert(is_fully_static(None))
|
||||
|
||||
static_assert(is_fully_static(Literal[1]))
|
||||
static_assert(is_fully_static(Literal[True]))
|
||||
static_assert(is_fully_static(Literal["abc"]))
|
||||
static_assert(is_fully_static(Literal[b"abc"]))
|
||||
|
||||
static_assert(is_fully_static(LiteralString))
|
||||
|
||||
static_assert(is_fully_static(str))
|
||||
static_assert(is_fully_static(object))
|
||||
static_assert(is_fully_static(type))
|
||||
|
||||
static_assert(is_fully_static(TypeOf[str]))
|
||||
static_assert(is_fully_static(TypeOf[Literal]))
|
||||
|
||||
static_assert(is_fully_static(str | None))
|
||||
static_assert(is_fully_static(Intersection[str, Not[LiteralString]]))
|
||||
|
||||
static_assert(is_fully_static(tuple[()]))
|
||||
static_assert(is_fully_static(tuple[int, object]))
|
||||
|
||||
static_assert(is_fully_static(type[str]))
|
||||
static_assert(is_fully_static(type[object]))
|
||||
```
|
||||
|
||||
## Non-fully-static
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Literal, LiteralString
|
||||
from knot_extensions import Intersection, Not, TypeOf, Unknown, is_fully_static, static_assert
|
||||
|
||||
static_assert(not is_fully_static(Any))
|
||||
static_assert(not is_fully_static(Unknown))
|
||||
|
||||
static_assert(not is_fully_static(Any | str))
|
||||
static_assert(not is_fully_static(str | Unknown))
|
||||
static_assert(not is_fully_static(Intersection[Any, Not[LiteralString]]))
|
||||
|
||||
static_assert(not is_fully_static(tuple[Any, ...]))
|
||||
static_assert(not is_fully_static(tuple[int, Any]))
|
||||
static_assert(not is_fully_static(type[Any]))
|
||||
```
|
||||
@@ -1,64 +0,0 @@
|
||||
# Gradual equivalence relation
|
||||
|
||||
Two gradual types `A` and `B` are equivalent if all [materializations] of `A` are also
|
||||
materializations of `B`, and all materializations of `B` are also materializations of `A`.
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal, LiteralString, Never
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy, TypeOf, Unknown, is_gradual_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Any, Any))
|
||||
static_assert(is_gradual_equivalent_to(Unknown, Unknown))
|
||||
static_assert(is_gradual_equivalent_to(Any, Unknown))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Never, Never))
|
||||
static_assert(is_gradual_equivalent_to(AlwaysTruthy, AlwaysTruthy))
|
||||
static_assert(is_gradual_equivalent_to(AlwaysFalsy, AlwaysFalsy))
|
||||
static_assert(is_gradual_equivalent_to(LiteralString, LiteralString))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(Literal[True], Literal[True]))
|
||||
static_assert(is_gradual_equivalent_to(Literal[False], Literal[False]))
|
||||
static_assert(is_gradual_equivalent_to(TypeOf[0:1:2], TypeOf[0:1:2]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(TypeOf[str], TypeOf[str]))
|
||||
static_assert(is_gradual_equivalent_to(type, type[object]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(type, type[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(type[object], type[Any]))
|
||||
```
|
||||
|
||||
## Unions and intersections
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from knot_extensions import Intersection, Not, Unknown, is_gradual_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_gradual_equivalent_to(str | int, str | int))
|
||||
static_assert(is_gradual_equivalent_to(str | int | Any, str | int | Unknown))
|
||||
static_assert(is_gradual_equivalent_to(str | int, int | str))
|
||||
static_assert(
|
||||
is_gradual_equivalent_to(Intersection[str, int, Not[bytes], Not[None]], Intersection[int, str, Not[None], Not[bytes]])
|
||||
)
|
||||
# TODO: `~type[Any]` shoudld be gradually equivalent to `~type[Unknown]`
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]], Intersection[int | str, Not[type[Unknown]]]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
|
||||
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, is_gradual_equivalent_to, static_assert
|
||||
|
||||
static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
|
||||
|
||||
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, bytes]))
|
||||
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
|
||||
```
|
||||
|
||||
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize
|
||||
@@ -1,25 +0,0 @@
|
||||
## Single-valued types
|
||||
|
||||
A type is single-valued iff it is not empty and all inhabitants of it compare equal.
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, Literal, LiteralString, Never
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
static_assert(is_single_valued(Literal[True]))
|
||||
static_assert(is_single_valued(Literal[1]))
|
||||
static_assert(is_single_valued(Literal["abc"]))
|
||||
static_assert(is_single_valued(Literal[b"abc"]))
|
||||
|
||||
static_assert(is_single_valued(tuple[()]))
|
||||
static_assert(is_single_valued(tuple[Literal[True], Literal[1]]))
|
||||
|
||||
static_assert(not is_single_valued(str))
|
||||
static_assert(not is_single_valued(Never))
|
||||
static_assert(not is_single_valued(Any))
|
||||
|
||||
static_assert(not is_single_valued(Literal[1, 2]))
|
||||
|
||||
static_assert(not is_single_valued(tuple[None, int]))
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
# Singleton types
|
||||
|
||||
A type is a singleton type iff it has exactly one inhabitant.
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, Never
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(None))
|
||||
static_assert(is_singleton(Literal[True]))
|
||||
static_assert(is_singleton(Literal[False]))
|
||||
|
||||
static_assert(is_singleton(type[bool]))
|
||||
|
||||
static_assert(not is_singleton(Never))
|
||||
static_assert(not is_singleton(str))
|
||||
|
||||
static_assert(not is_singleton(Literal[345]))
|
||||
static_assert(not is_singleton(Literal[1, 2]))
|
||||
|
||||
static_assert(not is_singleton(tuple[()]))
|
||||
static_assert(not is_singleton(tuple[None]))
|
||||
static_assert(not is_singleton(tuple[None, Literal[True]]))
|
||||
```
|
||||
|
||||
## `NoDefault`
|
||||
|
||||
### 3.12
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import _NoDefaultType
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(_NoDefaultType))
|
||||
```
|
||||
|
||||
### 3.13
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import _NoDefaultType
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
|
||||
static_assert(is_singleton(_NoDefaultType))
|
||||
```
|
||||
@@ -1,453 +0,0 @@
|
||||
# Subtype relation
|
||||
|
||||
The `is_subtype_of(S, T)` relation below checks if type `S` is a subtype of type `T`.
|
||||
|
||||
A fully static type `S` is a subtype of another fully static type `T` iff the set of values
|
||||
represented by `S` is a subset of the set of values represented by `T`.
|
||||
|
||||
See the [typing documentation] for more information.
|
||||
|
||||
## Basic builtin types
|
||||
|
||||
- `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a
|
||||
supertype of `bool` (present in `bool`s bases and MRO).
|
||||
- `int` is not a subtype of `float`/`complex`, even though `float`/`complex` can be used in place of
|
||||
`int` in some contexts (see [special case for float and complex]).
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(bool, bool))
|
||||
static_assert(is_subtype_of(bool, int))
|
||||
static_assert(is_subtype_of(bool, object))
|
||||
|
||||
static_assert(is_subtype_of(int, int))
|
||||
static_assert(is_subtype_of(int, object))
|
||||
|
||||
static_assert(is_subtype_of(object, object))
|
||||
|
||||
static_assert(not is_subtype_of(int, bool))
|
||||
static_assert(not is_subtype_of(int, str))
|
||||
static_assert(not is_subtype_of(object, int))
|
||||
|
||||
static_assert(not is_subtype_of(int, float))
|
||||
static_assert(not is_subtype_of(int, complex))
|
||||
|
||||
static_assert(is_subtype_of(TypeError, Exception))
|
||||
static_assert(is_subtype_of(FloatingPointError, Exception))
|
||||
```
|
||||
|
||||
## Class hierarchies
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
from typing_extensions import Never
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
class C(B1, B2): ...
|
||||
|
||||
static_assert(is_subtype_of(B1, A))
|
||||
static_assert(not is_subtype_of(A, B1))
|
||||
|
||||
static_assert(is_subtype_of(B2, A))
|
||||
static_assert(not is_subtype_of(A, B2))
|
||||
|
||||
static_assert(not is_subtype_of(B1, B2))
|
||||
static_assert(not is_subtype_of(B2, B1))
|
||||
|
||||
static_assert(is_subtype_of(C, B1))
|
||||
static_assert(is_subtype_of(C, B2))
|
||||
static_assert(not is_subtype_of(B1, C))
|
||||
static_assert(not is_subtype_of(B2, C))
|
||||
static_assert(is_subtype_of(C, A))
|
||||
static_assert(not is_subtype_of(A, C))
|
||||
|
||||
static_assert(is_subtype_of(Never, A))
|
||||
static_assert(is_subtype_of(Never, B1))
|
||||
static_assert(is_subtype_of(Never, B2))
|
||||
static_assert(is_subtype_of(Never, C))
|
||||
|
||||
static_assert(is_subtype_of(A, object))
|
||||
static_assert(is_subtype_of(B1, object))
|
||||
static_assert(is_subtype_of(B2, object))
|
||||
static_assert(is_subtype_of(C, object))
|
||||
```
|
||||
|
||||
## Literal types
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
# Boolean literals
|
||||
static_assert(is_subtype_of(Literal[True], bool))
|
||||
static_assert(is_subtype_of(Literal[True], int))
|
||||
static_assert(is_subtype_of(Literal[True], object))
|
||||
|
||||
# Integer literals
|
||||
static_assert(is_subtype_of(Literal[1], int))
|
||||
static_assert(is_subtype_of(Literal[1], object))
|
||||
|
||||
static_assert(not is_subtype_of(Literal[1], bool))
|
||||
|
||||
# See the note above (or link below) concerning int and float/complex
|
||||
static_assert(not is_subtype_of(Literal[1], float))
|
||||
|
||||
# String literals
|
||||
static_assert(is_subtype_of(Literal["foo"], LiteralString))
|
||||
static_assert(is_subtype_of(Literal["foo"], str))
|
||||
static_assert(is_subtype_of(Literal["foo"], object))
|
||||
|
||||
static_assert(is_subtype_of(LiteralString, str))
|
||||
static_assert(is_subtype_of(LiteralString, object))
|
||||
|
||||
# Bytes literals
|
||||
static_assert(is_subtype_of(Literal[b"foo"], bytes))
|
||||
static_assert(is_subtype_of(Literal[b"foo"], object))
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A1: ...
|
||||
class B1(A1): ...
|
||||
class A2: ...
|
||||
class B2(A2): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_subtype_of(B1, A1))
|
||||
static_assert(is_subtype_of(B2, A2))
|
||||
|
||||
# Zero-element tuples
|
||||
static_assert(is_subtype_of(tuple[()], tuple[()]))
|
||||
static_assert(not is_subtype_of(tuple[()], tuple[Unrelated]))
|
||||
|
||||
# One-element tuples
|
||||
static_assert(is_subtype_of(tuple[B1], tuple[A1]))
|
||||
static_assert(not is_subtype_of(tuple[B1], tuple[Unrelated]))
|
||||
static_assert(not is_subtype_of(tuple[B1], tuple[()]))
|
||||
static_assert(not is_subtype_of(tuple[B1], tuple[A1, Unrelated]))
|
||||
|
||||
# Two-element tuples
|
||||
static_assert(is_subtype_of(tuple[B1, B2], tuple[A1, A2]))
|
||||
static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, A2]))
|
||||
static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, Unrelated]))
|
||||
static_assert(not is_subtype_of(tuple[B1, B2], tuple[Unrelated, Unrelated]))
|
||||
static_assert(not is_subtype_of(tuple[B1, B2], tuple[()]))
|
||||
static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1]))
|
||||
static_assert(not is_subtype_of(tuple[B1, B2], tuple[A1, A2, Unrelated]))
|
||||
|
||||
static_assert(is_subtype_of(tuple[int], tuple))
|
||||
```
|
||||
|
||||
## Union types
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
class Unrelated1: ...
|
||||
class Unrelated2: ...
|
||||
|
||||
static_assert(is_subtype_of(B1, A))
|
||||
static_assert(is_subtype_of(B2, A))
|
||||
|
||||
# Union on the right hand side
|
||||
static_assert(is_subtype_of(B1, A | Unrelated1))
|
||||
static_assert(is_subtype_of(B1, Unrelated1 | A))
|
||||
|
||||
static_assert(not is_subtype_of(B1, Unrelated1 | Unrelated2))
|
||||
|
||||
# Union on the left hand side
|
||||
static_assert(is_subtype_of(B1 | B2, A))
|
||||
static_assert(is_subtype_of(B1 | B2 | A, object))
|
||||
|
||||
static_assert(not is_subtype_of(B1 | Unrelated1, A))
|
||||
static_assert(not is_subtype_of(Unrelated1 | B1, A))
|
||||
|
||||
# Union on both sides
|
||||
static_assert(is_subtype_of(B1 | bool, A | int))
|
||||
static_assert(is_subtype_of(B1 | bool, int | A))
|
||||
|
||||
static_assert(not is_subtype_of(B1 | bool, Unrelated1 | int))
|
||||
static_assert(not is_subtype_of(B1 | bool, int | Unrelated1))
|
||||
|
||||
# Example: Unions of literals
|
||||
static_assert(is_subtype_of(Literal[1, 2, 3], int))
|
||||
static_assert(not is_subtype_of(Literal[1, "two", 3], int))
|
||||
```
|
||||
|
||||
## Intersection types
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
class B2(A): ...
|
||||
class C(B1, B2): ...
|
||||
class Unrelated: ...
|
||||
|
||||
static_assert(is_subtype_of(B1, A))
|
||||
static_assert(is_subtype_of(B2, A))
|
||||
static_assert(is_subtype_of(C, A))
|
||||
static_assert(is_subtype_of(C, B1))
|
||||
static_assert(is_subtype_of(C, B2))
|
||||
|
||||
# For complements, the subtyping relation is reversed:
|
||||
static_assert(is_subtype_of(Not[A], Not[B1]))
|
||||
static_assert(is_subtype_of(Not[A], Not[B2]))
|
||||
static_assert(is_subtype_of(Not[A], Not[C]))
|
||||
static_assert(is_subtype_of(Not[B1], Not[C]))
|
||||
static_assert(is_subtype_of(Not[B2], Not[C]))
|
||||
|
||||
# The intersection of two types is a subtype of both:
|
||||
static_assert(is_subtype_of(Intersection[B1, B2], B1))
|
||||
static_assert(is_subtype_of(Intersection[B1, B2], B2))
|
||||
# … and of their common supertype:
|
||||
static_assert(is_subtype_of(Intersection[B1, B2], A))
|
||||
|
||||
# A common subtype of two types is a subtype of their intersection:
|
||||
static_assert(is_subtype_of(C, Intersection[B1, B2]))
|
||||
# … but not the other way around:
|
||||
static_assert(not is_subtype_of(Intersection[B1, B2], C))
|
||||
|
||||
# "Removing" B1 from A leaves a subtype of A.
|
||||
static_assert(is_subtype_of(Intersection[A, Not[B1]], A))
|
||||
static_assert(is_subtype_of(Intersection[A, Not[B1]], Not[B1]))
|
||||
|
||||
# B1 and B2 are not disjoint, so this is not true:
|
||||
static_assert(not is_subtype_of(B2, Intersection[A, Not[B1]]))
|
||||
# … but for two disjoint subtypes, it is:
|
||||
static_assert(is_subtype_of(Literal[2], Intersection[int, Not[Literal[1]]]))
|
||||
|
||||
# A and Unrelated are not related, so this is not true:
|
||||
static_assert(not is_subtype_of(Intersection[A, Not[B1]], Not[Unrelated]))
|
||||
# … but for a disjoint type like `None`, it is:
|
||||
static_assert(is_subtype_of(Intersection[A, Not[B1]], Not[None]))
|
||||
|
||||
# Complements of types are still subtypes of `object`:
|
||||
static_assert(is_subtype_of(Not[A], object))
|
||||
|
||||
# More examples:
|
||||
static_assert(is_subtype_of(type[str], Not[None]))
|
||||
static_assert(is_subtype_of(Not[LiteralString], object))
|
||||
|
||||
static_assert(not is_subtype_of(Intersection[int, Not[Literal[2]]], Intersection[int, Not[Literal[3]]]))
|
||||
static_assert(not is_subtype_of(Not[Literal[2]], Not[Literal[3]]))
|
||||
static_assert(not is_subtype_of(Not[Literal[2]], Not[int]))
|
||||
static_assert(not is_subtype_of(int, Not[Literal[3]]))
|
||||
static_assert(not is_subtype_of(Literal[1], Intersection[int, Not[Literal[1]]]))
|
||||
```
|
||||
|
||||
## Special types
|
||||
|
||||
### `Never`
|
||||
|
||||
`Never` is a subtype of all types.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, Never
|
||||
from knot_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(Never, Never))
|
||||
static_assert(is_subtype_of(Never, Literal[True]))
|
||||
static_assert(is_subtype_of(Never, bool))
|
||||
static_assert(is_subtype_of(Never, int))
|
||||
static_assert(is_subtype_of(Never, object))
|
||||
|
||||
static_assert(is_subtype_of(Never, AlwaysTruthy))
|
||||
static_assert(is_subtype_of(Never, AlwaysFalsy))
|
||||
```
|
||||
|
||||
### `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
```py
|
||||
from knot_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(Literal[1], AlwaysTruthy))
|
||||
static_assert(is_subtype_of(Literal[0], AlwaysFalsy))
|
||||
|
||||
static_assert(is_subtype_of(AlwaysTruthy, object))
|
||||
static_assert(is_subtype_of(AlwaysFalsy, object))
|
||||
|
||||
static_assert(not is_subtype_of(Literal[1], AlwaysFalsy))
|
||||
static_assert(not is_subtype_of(Literal[0], AlwaysTruthy))
|
||||
|
||||
static_assert(not is_subtype_of(str, AlwaysTruthy))
|
||||
static_assert(not is_subtype_of(str, AlwaysFalsy))
|
||||
```
|
||||
|
||||
### Module literals
|
||||
|
||||
```py
|
||||
from types import ModuleType
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
from typing_extensions import assert_type
|
||||
import typing
|
||||
|
||||
assert_type(typing, TypeOf[typing])
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[typing], ModuleType))
|
||||
```
|
||||
|
||||
### Slice literals
|
||||
|
||||
```py
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[1:2:3], slice))
|
||||
```
|
||||
|
||||
### Special forms
|
||||
|
||||
```py
|
||||
from typing import _SpecialForm
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[Literal], _SpecialForm))
|
||||
static_assert(is_subtype_of(TypeOf[Literal], object))
|
||||
|
||||
static_assert(not is_subtype_of(_SpecialForm, TypeOf[Literal]))
|
||||
```
|
||||
|
||||
## Class literal types and `type[…]`
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
from typing import _SpecialForm
|
||||
from typing_extensions import Literal, assert_type
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
class Meta(type): ...
|
||||
class HasCustomMetaclass(metaclass=Meta): ...
|
||||
|
||||
type LiteralBool = TypeOf[bool]
|
||||
type LiteralInt = TypeOf[int]
|
||||
type LiteralStr = TypeOf[str]
|
||||
type LiteralObject = TypeOf[object]
|
||||
|
||||
assert_type(bool, LiteralBool)
|
||||
assert_type(int, LiteralInt)
|
||||
assert_type(str, LiteralStr)
|
||||
assert_type(object, LiteralObject)
|
||||
|
||||
# bool
|
||||
|
||||
static_assert(is_subtype_of(LiteralBool, LiteralBool))
|
||||
static_assert(is_subtype_of(LiteralBool, type[bool]))
|
||||
static_assert(is_subtype_of(LiteralBool, type[int]))
|
||||
static_assert(is_subtype_of(LiteralBool, type[object]))
|
||||
static_assert(is_subtype_of(LiteralBool, type))
|
||||
static_assert(is_subtype_of(LiteralBool, object))
|
||||
|
||||
static_assert(not is_subtype_of(LiteralBool, LiteralInt))
|
||||
static_assert(not is_subtype_of(LiteralBool, LiteralObject))
|
||||
static_assert(not is_subtype_of(LiteralBool, bool))
|
||||
|
||||
static_assert(not is_subtype_of(type, type[bool]))
|
||||
|
||||
# int
|
||||
|
||||
static_assert(is_subtype_of(LiteralInt, LiteralInt))
|
||||
static_assert(is_subtype_of(LiteralInt, type[int]))
|
||||
static_assert(is_subtype_of(LiteralInt, type[object]))
|
||||
static_assert(is_subtype_of(LiteralInt, type))
|
||||
static_assert(is_subtype_of(LiteralInt, object))
|
||||
|
||||
static_assert(not is_subtype_of(LiteralInt, LiteralObject))
|
||||
static_assert(not is_subtype_of(LiteralInt, int))
|
||||
|
||||
static_assert(not is_subtype_of(type, type[int]))
|
||||
|
||||
# LiteralString
|
||||
|
||||
static_assert(is_subtype_of(LiteralStr, type[str]))
|
||||
static_assert(is_subtype_of(LiteralStr, type))
|
||||
static_assert(is_subtype_of(LiteralStr, type[object]))
|
||||
|
||||
static_assert(not is_subtype_of(type[str], LiteralStr))
|
||||
|
||||
# custom meta classes
|
||||
|
||||
type LiteralHasCustomMetaclass = TypeOf[HasCustomMetaclass]
|
||||
|
||||
static_assert(is_subtype_of(LiteralHasCustomMetaclass, Meta))
|
||||
static_assert(is_subtype_of(Meta, type[object]))
|
||||
static_assert(is_subtype_of(Meta, type))
|
||||
|
||||
static_assert(not is_subtype_of(Meta, type[type]))
|
||||
```
|
||||
|
||||
### Unions of class literals
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
type LiteralBase = TypeOf[Base]
|
||||
type LiteralDerived = TypeOf[Derived]
|
||||
type LiteralUnrelated = TypeOf[Unrelated]
|
||||
|
||||
assert_type(Base, LiteralBase)
|
||||
assert_type(Derived, LiteralDerived)
|
||||
assert_type(Unrelated, LiteralUnrelated)
|
||||
|
||||
static_assert(is_subtype_of(LiteralBase, type))
|
||||
static_assert(is_subtype_of(LiteralBase, object))
|
||||
|
||||
static_assert(is_subtype_of(LiteralBase, type[Base]))
|
||||
static_assert(is_subtype_of(LiteralDerived, type[Base]))
|
||||
static_assert(is_subtype_of(LiteralDerived, type[Derived]))
|
||||
|
||||
static_assert(not is_subtype_of(LiteralBase, type[Derived]))
|
||||
static_assert(is_subtype_of(type[Derived], type[Base]))
|
||||
|
||||
static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, type))
|
||||
static_assert(is_subtype_of(LiteralBase | LiteralUnrelated, object))
|
||||
```
|
||||
|
||||
## Non-fully-static types
|
||||
|
||||
`Any`, `Unknown`, `Todo` and derivatives thereof do not participate in subtyping.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, is_subtype_of, static_assert, Intersection
|
||||
from typing_extensions import Any
|
||||
|
||||
static_assert(not is_subtype_of(Any, Any))
|
||||
static_assert(not is_subtype_of(Any, int))
|
||||
static_assert(not is_subtype_of(int, Any))
|
||||
static_assert(not is_subtype_of(Any, object))
|
||||
static_assert(not is_subtype_of(object, Any))
|
||||
|
||||
static_assert(not is_subtype_of(int, Any | int))
|
||||
static_assert(not is_subtype_of(Intersection[Any, int], int))
|
||||
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any]))
|
||||
|
||||
# The same for `Unknown`:
|
||||
static_assert(not is_subtype_of(Unknown, Unknown))
|
||||
static_assert(not is_subtype_of(Unknown, int))
|
||||
static_assert(not is_subtype_of(int, Unknown))
|
||||
static_assert(not is_subtype_of(Unknown, object))
|
||||
static_assert(not is_subtype_of(object, Unknown))
|
||||
|
||||
static_assert(not is_subtype_of(int, Unknown | int))
|
||||
static_assert(not is_subtype_of(Intersection[Unknown, int], int))
|
||||
static_assert(not is_subtype_of(tuple[int, int], tuple[int, Unknown]))
|
||||
```
|
||||
|
||||
[special case for float and complex]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
@@ -1,27 +0,0 @@
|
||||
# `__str__` and `__repr__`
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def _(
|
||||
a: Literal[1],
|
||||
b: Literal[True],
|
||||
c: Literal[False],
|
||||
d: Literal["ab'cd"],
|
||||
e: LiteralString,
|
||||
f: int,
|
||||
):
|
||||
reveal_type(str(a)) # revealed: Literal["1"]
|
||||
reveal_type(str(b)) # revealed: Literal["True"]
|
||||
reveal_type(str(c)) # revealed: Literal["False"]
|
||||
reveal_type(str(d)) # revealed: Literal["ab'cd"]
|
||||
reveal_type(str(e)) # revealed: LiteralString
|
||||
reveal_type(str(f)) # revealed: str
|
||||
|
||||
reveal_type(repr(a)) # revealed: Literal["1"]
|
||||
reveal_type(repr(b)) # revealed: Literal["True"]
|
||||
reveal_type(repr(c)) # revealed: Literal["False"]
|
||||
reveal_type(repr(d)) # revealed: Literal["'ab\\'cd'"]
|
||||
reveal_type(repr(e)) # revealed: LiteralString
|
||||
reveal_type(repr(f)) # revealed: str
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
# Truthiness
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy
|
||||
|
||||
def _(
|
||||
a: Literal[1],
|
||||
b: Literal[-1],
|
||||
c: Literal["foo"],
|
||||
d: tuple[Literal[0]],
|
||||
e: Literal[1, 2],
|
||||
f: AlwaysTruthy,
|
||||
):
|
||||
reveal_type(bool(a)) # revealed: Literal[True]
|
||||
reveal_type(bool(b)) # revealed: Literal[True]
|
||||
reveal_type(bool(c)) # revealed: Literal[True]
|
||||
reveal_type(bool(d)) # revealed: Literal[True]
|
||||
reveal_type(bool(e)) # revealed: Literal[True]
|
||||
reveal_type(bool(f)) # revealed: Literal[True]
|
||||
|
||||
def _(
|
||||
a: tuple[()],
|
||||
b: Literal[0],
|
||||
c: Literal[""],
|
||||
d: Literal[b""],
|
||||
e: Literal[0, 0],
|
||||
f: AlwaysFalsy,
|
||||
):
|
||||
reveal_type(bool(a)) # revealed: Literal[False]
|
||||
reveal_type(bool(b)) # revealed: Literal[False]
|
||||
reveal_type(bool(c)) # revealed: Literal[False]
|
||||
reveal_type(bool(d)) # revealed: Literal[False]
|
||||
reveal_type(bool(e)) # revealed: Literal[False]
|
||||
reveal_type(bool(f)) # revealed: Literal[False]
|
||||
|
||||
def _(
|
||||
a: str,
|
||||
b: Literal[1, 0],
|
||||
c: str | Literal[0],
|
||||
d: str | Literal[1],
|
||||
):
|
||||
reveal_type(bool(a)) # revealed: bool
|
||||
reveal_type(bool(b)) # revealed: bool
|
||||
reveal_type(bool(c)) # revealed: bool
|
||||
reveal_type(bool(d)) # revealed: bool
|
||||
```
|
||||
@@ -1,93 +0,0 @@
|
||||
# `typing.ClassVar`
|
||||
|
||||
[`typing.ClassVar`] is a type qualifier that is used to indicate that a class variable may not be
|
||||
written to from instances of that class.
|
||||
|
||||
This test makes sure that we discover the type qualifier while inferring types from an annotation.
|
||||
For more details on the semantics of pure class variables, see [this test](../attributes.md).
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing import ClassVar, Annotated
|
||||
|
||||
class C:
|
||||
a: ClassVar[int] = 1
|
||||
b: Annotated[ClassVar[int], "the annotation for b"] = 1
|
||||
c: ClassVar[Annotated[int, "the annotation for c"]] = 1
|
||||
d: ClassVar = 1
|
||||
e: "ClassVar[int]" = 1
|
||||
|
||||
reveal_type(C.a) # revealed: int
|
||||
reveal_type(C.b) # revealed: int
|
||||
reveal_type(C.c) # revealed: int
|
||||
# TODO: should be Unknown | Literal[1]
|
||||
reveal_type(C.d) # revealed: Unknown
|
||||
reveal_type(C.e) # revealed: int
|
||||
|
||||
c = C()
|
||||
|
||||
# error: [invalid-attribute-access]
|
||||
c.a = 2
|
||||
# error: [invalid-attribute-access]
|
||||
c.b = 2
|
||||
# error: [invalid-attribute-access]
|
||||
c.c = 2
|
||||
# error: [invalid-attribute-access]
|
||||
c.d = 2
|
||||
# error: [invalid-attribute-access]
|
||||
c.e = 2
|
||||
```
|
||||
|
||||
## Conflicting type qualifiers
|
||||
|
||||
We currently ignore conflicting qualifiers and simply union them, which is more conservative than
|
||||
intersecting them. This means that we consider `a` to be a `ClassVar` here:
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class C:
|
||||
if flag():
|
||||
a: ClassVar[int] = 1
|
||||
else:
|
||||
a: str
|
||||
|
||||
reveal_type(C.a) # revealed: int | str
|
||||
|
||||
c = C()
|
||||
|
||||
# error: [invalid-attribute-access]
|
||||
c.a = 2
|
||||
```
|
||||
|
||||
## Too many arguments
|
||||
|
||||
```py
|
||||
class C:
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` expects exactly one type parameter"
|
||||
x: ClassVar[int, str] = 1
|
||||
```
|
||||
|
||||
## Illegal `ClassVar` in type expression
|
||||
|
||||
```py
|
||||
class C:
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
||||
x: ClassVar | int
|
||||
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
||||
y: int | ClassVar[str]
|
||||
```
|
||||
|
||||
## Used outside of a class
|
||||
|
||||
```py
|
||||
# TODO: this should be an error
|
||||
x: ClassVar[int] = 1
|
||||
```
|
||||
|
||||
[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar
|
||||
@@ -175,12 +175,12 @@ pub(crate) mod tests {
|
||||
db.write_files(self.files)
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(vec![src_root]);
|
||||
let mut search_paths = SearchPathSettings::new(src_root);
|
||||
search_paths.typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
&ProgramSettings {
|
||||
python_version: self.python_version,
|
||||
python_platform: self.python_platform,
|
||||
search_paths,
|
||||
|
||||
@@ -154,10 +154,6 @@ impl KnownModule {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_builtins(self) -> bool {
|
||||
matches!(self, Self::Builtins)
|
||||
}
|
||||
|
||||
pub const fn is_typing(self) -> bool {
|
||||
matches!(self, Self::Typing)
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ impl SearchPaths {
|
||||
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
src_roots,
|
||||
src_root,
|
||||
typeshed,
|
||||
site_packages: site_packages_paths,
|
||||
} = settings;
|
||||
@@ -186,10 +186,8 @@ impl SearchPaths {
|
||||
static_paths.push(SearchPath::extra(system, path)?);
|
||||
}
|
||||
|
||||
for src_root in src_roots {
|
||||
tracing::debug!("Adding first-party search path '{src_root}'");
|
||||
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
|
||||
}
|
||||
tracing::debug!("Adding first-party search path '{src_root}'");
|
||||
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
|
||||
|
||||
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
|
||||
let typeshed = canonicalize(typeshed, system);
|
||||
@@ -222,14 +220,8 @@ impl SearchPaths {
|
||||
static_paths.push(stdlib_path);
|
||||
|
||||
let site_packages_paths = match site_packages_paths {
|
||||
SitePackages::Derived { venv_path } => {
|
||||
// TODO: We may want to warn here if the venv's python version is older
|
||||
// than the one resolved in the program settings because it indicates
|
||||
// that the `target-version` is incorrectly configured or that the
|
||||
// venv is out of date.
|
||||
VirtualEnvironment::new(venv_path, system)
|
||||
.and_then(|venv| venv.site_packages_directories(system))?
|
||||
}
|
||||
SitePackages::Derived { venv_path } => VirtualEnvironment::new(venv_path, system)
|
||||
.and_then(|venv| venv.site_packages_directories(system))?,
|
||||
SitePackages::Known(paths) => paths
|
||||
.iter()
|
||||
.map(|path| canonicalize(path, system))
|
||||
@@ -1302,12 +1294,12 @@ mod tests {
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::PY38,
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![src.clone()],
|
||||
src_root: src.clone(),
|
||||
typeshed: Some(custom_typeshed),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
@@ -1808,12 +1800,12 @@ not_a_directory
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![SystemPathBuf::from("/src")],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![
|
||||
venv_site_packages,
|
||||
|
||||
@@ -232,12 +232,12 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![src.clone()],
|
||||
src_root: src.clone(),
|
||||
typeshed: Some(typeshed.clone()),
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
},
|
||||
@@ -290,12 +290,12 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths: SearchPathSettings {
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
..SearchPathSettings::new(vec![src.clone()])
|
||||
..SearchPathSettings::new(src.clone())
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::module_resolver::SearchPaths;
|
||||
use crate::python_platform::PythonPlatform;
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::Db;
|
||||
|
||||
use anyhow::Context;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
|
||||
use crate::module_resolver::SearchPaths;
|
||||
use crate::Db;
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
pub struct Program {
|
||||
pub python_version: PythonVersion,
|
||||
|
||||
#[return_ref]
|
||||
pub python_platform: PythonPlatform,
|
||||
|
||||
#[return_ref]
|
||||
@@ -20,51 +20,25 @@ pub struct Program {
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> anyhow::Result<Self> {
|
||||
pub fn from_settings(db: &dyn Db, settings: &ProgramSettings) -> anyhow::Result<Self> {
|
||||
let ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths,
|
||||
} = settings;
|
||||
|
||||
tracing::info!("Python version: Python {python_version}, platform: {python_platform}");
|
||||
tracing::info!("Python version: Python {python_version}");
|
||||
|
||||
let search_paths = SearchPaths::from_settings(db, &search_paths)
|
||||
let search_paths = SearchPaths::from_settings(db, search_paths)
|
||||
.with_context(|| "Invalid search path settings")?;
|
||||
|
||||
Ok(
|
||||
Program::builder(python_version, python_platform, search_paths)
|
||||
Program::builder(*python_version, python_platform.clone(), search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_from_settings(
|
||||
self,
|
||||
db: &mut dyn Db,
|
||||
settings: ProgramSettings,
|
||||
) -> anyhow::Result<()> {
|
||||
let ProgramSettings {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths,
|
||||
} = settings;
|
||||
|
||||
if &python_platform != self.python_platform(db) {
|
||||
tracing::debug!("Updating python platform: `{python_platform:?}`");
|
||||
self.set_python_platform(db).to(python_platform);
|
||||
}
|
||||
|
||||
if python_version != self.python_version(db) {
|
||||
tracing::debug!("Updating python version: Python {python_version}");
|
||||
self.set_python_version(db).to(python_version);
|
||||
}
|
||||
|
||||
self.update_search_paths(db, &search_paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_search_paths(
|
||||
self,
|
||||
db: &mut dyn Db,
|
||||
@@ -102,8 +76,8 @@ pub struct SearchPathSettings {
|
||||
/// or pyright's stubPath configuration setting.
|
||||
pub extra_paths: Vec<SystemPathBuf>,
|
||||
|
||||
/// The root of the project, used for finding first-party modules.
|
||||
pub src_roots: Vec<SystemPathBuf>,
|
||||
/// The root of the workspace, used for finding first-party modules.
|
||||
pub src_root: SystemPathBuf,
|
||||
|
||||
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
@@ -115,9 +89,9 @@ pub struct SearchPathSettings {
|
||||
}
|
||||
|
||||
impl SearchPathSettings {
|
||||
pub fn new(src_roots: Vec<SystemPathBuf>) -> Self {
|
||||
pub fn new(src_root: SystemPathBuf) -> Self {
|
||||
Self {
|
||||
src_roots,
|
||||
src_root,
|
||||
extra_paths: vec![],
|
||||
typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![]),
|
||||
@@ -126,7 +100,7 @@ impl SearchPathSettings {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum SitePackages {
|
||||
Derived {
|
||||
venv_path: SystemPathBuf,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// The target platform to assume when resolving types.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(
|
||||
@@ -19,12 +17,3 @@ pub enum PythonPlatform {
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
Identifier(String),
|
||||
}
|
||||
|
||||
impl Display for PythonPlatform {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PythonPlatform::All => f.write_str("all"),
|
||||
PythonPlatform::Identifier(name) => f.write_str(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_index::newtype_index;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::ExprRef;
|
||||
use ruff_python_ast::ExpressionRef;
|
||||
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::semantic_index;
|
||||
@@ -60,12 +60,12 @@ pub trait HasScopedUseId {
|
||||
|
||||
impl HasScopedUseId for ast::ExprName {
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||
let expression_ref = ExprRef::from(self);
|
||||
let expression_ref = ExpressionRef::from(self);
|
||||
expression_ref.scoped_use_id(db, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasScopedUseId for ast::ExprRef<'_> {
|
||||
impl HasScopedUseId for ast::ExpressionRef<'_> {
|
||||
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
|
||||
let ast_ids = ast_ids(db, scope);
|
||||
ast_ids.use_id(*self)
|
||||
@@ -91,7 +91,7 @@ macro_rules! impl_has_scoped_expression_id {
|
||||
($ty: ty) => {
|
||||
impl HasScopedExpressionId for $ty {
|
||||
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||
let expression_ref = ExprRef::from(self);
|
||||
let expression_ref = ExpressionRef::from(self);
|
||||
expression_ref.scoped_expression_id(db, scope)
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ impl_has_scoped_expression_id!(ast::ExprSlice);
|
||||
impl_has_scoped_expression_id!(ast::ExprIpyEscapeCommand);
|
||||
impl_has_scoped_expression_id!(ast::Expr);
|
||||
|
||||
impl HasScopedExpressionId for ast::ExprRef<'_> {
|
||||
impl HasScopedExpressionId for ast::ExpressionRef<'_> {
|
||||
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
|
||||
let ast_ids = ast_ids(db, scope);
|
||||
ast_ids.expression_id(*self)
|
||||
@@ -184,8 +184,8 @@ pub(crate) mod node_key {
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub(crate) struct ExpressionNodeKey(NodeKey);
|
||||
|
||||
impl From<ast::ExprRef<'_>> for ExpressionNodeKey {
|
||||
fn from(value: ast::ExprRef<'_>) -> Self {
|
||||
impl From<ast::ExpressionRef<'_>> for ExpressionNodeKey {
|
||||
fn from(value: ast::ExpressionRef<'_>) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
use crate::{Db, KnownModule};
|
||||
|
||||
/// A definition of a symbol.
|
||||
///
|
||||
@@ -60,6 +61,24 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_binding()
|
||||
}
|
||||
|
||||
pub(crate) fn is_builtin_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::Builtins))
|
||||
}
|
||||
|
||||
/// Return true if this symbol was defined in the `typing` or `typing_extensions` modules
|
||||
pub(crate) fn is_typing_definition(self, db: &'db dyn Db) -> bool {
|
||||
matches!(
|
||||
file_to_module(db, self.file(db)).and_then(|module| module.known()),
|
||||
Some(KnownModule::Typing | KnownModule::TypingExtensions)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_knot_extensions_definition(self, db: &'db dyn Db) -> bool {
|
||||
file_to_module(db, self.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::KnotExtensions))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
||||
@@ -93,19 +93,6 @@ impl<const B: usize> BitSet<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Union in-place with another [`BitSet`].
|
||||
pub(super) fn union(&mut self, other: &BitSet<B>) {
|
||||
let mut max_len = self.blocks().len();
|
||||
let other_len = other.blocks().len();
|
||||
if other_len > max_len {
|
||||
max_len = other_len;
|
||||
self.resize_blocks(max_len);
|
||||
}
|
||||
for (my_block, other_block) in self.blocks_mut().iter_mut().zip(other.blocks()) {
|
||||
*my_block |= other_block;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
|
||||
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
|
||||
let blocks = self.blocks();
|
||||
@@ -235,59 +222,6 @@ mod tests {
|
||||
assert_bitset(&b1, &[89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
let mut b1 = BitSet::<1>::with(2);
|
||||
let b2 = BitSet::<1>::with(4);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[2, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_1() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(5);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 5, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_2() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(89);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 23, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 89, 90]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap_2() {
|
||||
let mut b1 = BitSet::<1>::with(89);
|
||||
let mut b2 = BitSet::<1>::with(89);
|
||||
b1.insert(91);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[89, 90, 91]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_blocks() {
|
||||
let mut b = BitSet::<2>::with(120);
|
||||
|
||||
@@ -310,15 +310,12 @@ impl SymbolState {
|
||||
visibility_constraints: VisibilityConstraintPerBinding::default(),
|
||||
},
|
||||
declarations: SymbolDeclarations {
|
||||
live_declarations: self.declarations.live_declarations.clone(),
|
||||
live_declarations: Declarations::default(),
|
||||
visibility_constraints: VisibilityConstraintPerDeclaration::default(),
|
||||
},
|
||||
};
|
||||
|
||||
std::mem::swap(&mut a, self);
|
||||
self.declarations
|
||||
.live_declarations
|
||||
.union(&b.declarations.live_declarations);
|
||||
|
||||
let mut a_defs_iter = a.bindings.live_bindings.iter();
|
||||
let mut b_defs_iter = b.bindings.live_bindings.iter();
|
||||
@@ -452,8 +449,10 @@ impl SymbolState {
|
||||
let mut opt_a_decl: Option<u32> = a_decls_iter.next();
|
||||
let mut opt_b_decl: Option<u32> = b_decls_iter.next();
|
||||
|
||||
let push = |vis_constraints_iter: &mut VisibilityConstraintsIntoIterator,
|
||||
let push = |decl,
|
||||
vis_constraints_iter: &mut VisibilityConstraintsIntoIterator,
|
||||
merged: &mut Self| {
|
||||
merged.declarations.live_declarations.insert(decl);
|
||||
let vis_constraints = vis_constraints_iter
|
||||
.next()
|
||||
.expect("declarations and visibility_constraints length mismatch");
|
||||
@@ -467,15 +466,15 @@ impl SymbolState {
|
||||
match (opt_a_decl, opt_b_decl) {
|
||||
(Some(a_decl), Some(b_decl)) => match a_decl.cmp(&b_decl) {
|
||||
std::cmp::Ordering::Less => {
|
||||
push(&mut a_vis_constraints_iter, self);
|
||||
push(a_decl, &mut a_vis_constraints_iter, self);
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
push(&mut b_vis_constraints_iter, self);
|
||||
push(b_decl, &mut b_vis_constraints_iter, self);
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
push(&mut b_vis_constraints_iter, self);
|
||||
push(a_decl, &mut b_vis_constraints_iter, self);
|
||||
|
||||
let a_vis_constraint = a_vis_constraints_iter
|
||||
.next()
|
||||
@@ -488,12 +487,12 @@ impl SymbolState {
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
push(&mut a_vis_constraints_iter, self);
|
||||
(Some(a_decl), None) => {
|
||||
push(a_decl, &mut a_vis_constraints_iter, self);
|
||||
opt_a_decl = a_decls_iter.next();
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
push(&mut b_vis_constraints_iter, self);
|
||||
(None, Some(b_decl)) => {
|
||||
push(b_decl, &mut b_vis_constraints_iter, self);
|
||||
opt_b_decl = b_decls_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_db::files::{File, FilePath};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, ExprRef};
|
||||
use ruff_python_ast::{Expr, ExpressionRef};
|
||||
use ruff_source_file::LineIndex;
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
@@ -48,7 +48,7 @@ pub trait HasTy {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>;
|
||||
}
|
||||
|
||||
impl HasTy for ast::ExprRef<'_> {
|
||||
impl HasTy for ast::ExpressionRef<'_> {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let file_scope = index.expression_scope_id(*self);
|
||||
@@ -64,7 +64,7 @@ macro_rules! impl_expression_has_ty {
|
||||
impl HasTy for $ty {
|
||||
#[inline]
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let expression_ref = ExprRef::from(self);
|
||||
let expression_ref = ExpressionRef::from(self);
|
||||
expression_ref.ty(model)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,8 @@ use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::Truthiness;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -246,164 +248,83 @@ struct InnerIntersectionBuilder<'db> {
|
||||
|
||||
impl<'db> InnerIntersectionBuilder<'db> {
|
||||
/// Adds a positive type to this intersection.
|
||||
fn add_positive(&mut self, db: &'db dyn Db, mut new_positive: Type<'db>) {
|
||||
match new_positive {
|
||||
// `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]`
|
||||
Type::AlwaysTruthy if self.positive.contains(&Type::LiteralString) => {
|
||||
self.add_negative(db, Type::string_literal(db, ""));
|
||||
fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) {
|
||||
if let Type::Intersection(other) = new_positive {
|
||||
for pos in other.positive(db) {
|
||||
self.add_positive(db, *pos);
|
||||
}
|
||||
// `LiteralString & AlwaysFalsy` -> `Literal[""]`
|
||||
Type::AlwaysFalsy if self.positive.swap_remove(&Type::LiteralString) => {
|
||||
self.add_positive(db, Type::string_literal(db, ""));
|
||||
for neg in other.negative(db) {
|
||||
self.add_negative(db, *neg);
|
||||
}
|
||||
// `AlwaysTruthy & LiteralString` -> `LiteralString & ~Literal[""]`
|
||||
Type::LiteralString if self.positive.swap_remove(&Type::AlwaysTruthy) => {
|
||||
self.add_positive(db, Type::LiteralString);
|
||||
self.add_negative(db, Type::string_literal(db, ""));
|
||||
}
|
||||
// `AlwaysFalsy & LiteralString` -> `Literal[""]`
|
||||
Type::LiteralString if self.positive.swap_remove(&Type::AlwaysFalsy) => {
|
||||
self.add_positive(db, Type::string_literal(db, ""));
|
||||
}
|
||||
// `LiteralString & ~AlwaysTruthy` -> `LiteralString & AlwaysFalsy` -> `Literal[""]`
|
||||
Type::LiteralString if self.negative.swap_remove(&Type::AlwaysTruthy) => {
|
||||
self.add_positive(db, Type::string_literal(db, ""));
|
||||
}
|
||||
// `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]`
|
||||
Type::LiteralString if self.negative.swap_remove(&Type::AlwaysFalsy) => {
|
||||
self.add_positive(db, Type::LiteralString);
|
||||
self.add_negative(db, Type::string_literal(db, ""));
|
||||
}
|
||||
// `(A & B & ~C) & (D & E & ~F)` -> `A & B & D & E & ~C & ~F`
|
||||
Type::Intersection(other) => {
|
||||
for pos in other.positive(db) {
|
||||
self.add_positive(db, *pos);
|
||||
}
|
||||
for neg in other.negative(db) {
|
||||
self.add_negative(db, *neg);
|
||||
} else {
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
// ~AlwaysTruthy & bool = Literal[False]
|
||||
if let Type::Instance(InstanceType { class }) = new_positive {
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
if let Some(new_type) = self
|
||||
.negative
|
||||
.iter()
|
||||
.find(|element| {
|
||||
element.is_boolean_literal()
|
||||
| matches!(element, Type::AlwaysFalsy | Type::AlwaysTruthy)
|
||||
})
|
||||
.map(|element| {
|
||||
Type::BooleanLiteral(element.bool(db) != Truthiness::AlwaysTrue)
|
||||
})
|
||||
{
|
||||
*self = Self::default();
|
||||
self.positive.insert(new_type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let known_instance = new_positive
|
||||
.into_instance()
|
||||
.and_then(|instance| instance.class.known(db));
|
||||
|
||||
if known_instance == Some(KnownClass::Object) {
|
||||
// `object & T` -> `T`; it is always redundant to add `object` to an intersection
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive)
|
||||
|| existing_positive.is_same_gradual_form(new_positive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let addition_is_bool_instance = known_instance == Some(KnownClass::Bool);
|
||||
|
||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
match existing_positive {
|
||||
// `AlwaysTruthy & bool` -> `Literal[True]`
|
||||
Type::AlwaysTruthy if addition_is_bool_instance => {
|
||||
new_positive = Type::BooleanLiteral(true);
|
||||
}
|
||||
// `AlwaysFalsy & bool` -> `Literal[False]`
|
||||
Type::AlwaysFalsy if addition_is_bool_instance => {
|
||||
new_positive = Type::BooleanLiteral(false);
|
||||
}
|
||||
Type::Instance(InstanceType { class })
|
||||
if class.is_known(db, KnownClass::Bool) =>
|
||||
{
|
||||
match new_positive {
|
||||
// `bool & AlwaysTruthy` -> `Literal[True]`
|
||||
Type::AlwaysTruthy => {
|
||||
new_positive = Type::BooleanLiteral(true);
|
||||
}
|
||||
// `bool & AlwaysFalsy` -> `Literal[False]`
|
||||
Type::AlwaysFalsy => {
|
||||
new_positive = Type::BooleanLiteral(false);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
self.positive.swap_remove_index(index);
|
||||
break;
|
||||
// same rule, reverse order
|
||||
if new_positive.is_subtype_of(db, *existing_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
|
||||
if addition_is_bool_instance {
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
match existing_negative {
|
||||
// `bool & ~Literal[False]` -> `Literal[True]`
|
||||
// `bool & ~Literal[True]` -> `Literal[False]`
|
||||
Type::BooleanLiteral(bool_value) => {
|
||||
new_positive = Type::BooleanLiteral(!bool_value);
|
||||
}
|
||||
// `bool & ~AlwaysTruthy` -> `Literal[False]`
|
||||
Type::AlwaysTruthy => {
|
||||
new_positive = Type::BooleanLiteral(false);
|
||||
}
|
||||
// `bool & ~AlwaysFalsy` -> `Literal[True]`
|
||||
Type::AlwaysFalsy => {
|
||||
new_positive = Type::BooleanLiteral(true);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
self.negative.swap_remove_index(index);
|
||||
break;
|
||||
}
|
||||
// A & B = Never if A and B are disjoint
|
||||
if new_positive.is_disjoint_from(db, *existing_positive) {
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive)
|
||||
|| existing_positive.is_same_gradual_form(new_positive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// same rule, reverse order
|
||||
if new_positive.is_subtype_of(db, *existing_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
// A & B = Never if A and B are disjoint
|
||||
if new_positive.is_disjoint_from(db, *existing_positive) {
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for index in to_remove.into_iter().rev() {
|
||||
self.positive.swap_remove_index(index);
|
||||
}
|
||||
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// S & ~T = Never if S <: T
|
||||
if new_positive.is_subtype_of(db, *existing_negative) {
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
// A & ~B = A if A and B are disjoint
|
||||
if existing_negative.is_disjoint_from(db, new_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
}
|
||||
for index in to_remove.into_iter().rev() {
|
||||
self.negative.swap_remove_index(index);
|
||||
}
|
||||
|
||||
self.positive.insert(new_positive);
|
||||
}
|
||||
for index in to_remove.iter().rev() {
|
||||
self.positive.swap_remove_index(*index);
|
||||
}
|
||||
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// S & ~T = Never if S <: T
|
||||
if new_positive.is_subtype_of(db, *existing_negative) {
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
// A & ~B = A if A and B are disjoint
|
||||
if existing_negative.is_disjoint_from(db, new_positive) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
}
|
||||
for index in to_remove.iter().rev() {
|
||||
self.negative.swap_remove_index(*index);
|
||||
}
|
||||
|
||||
self.positive.insert(new_positive);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a negative type to this intersection.
|
||||
fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) {
|
||||
let contains_bool = || {
|
||||
self.positive
|
||||
.iter()
|
||||
.filter_map(|ty| ty.into_instance())
|
||||
.filter_map(|instance| instance.class.known(db))
|
||||
.any(KnownClass::is_bool)
|
||||
};
|
||||
|
||||
match new_negative {
|
||||
Type::Intersection(inter) => {
|
||||
for pos in inter.positive(db) {
|
||||
@@ -427,23 +348,15 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
// simplify the representation.
|
||||
self.add_positive(db, ty);
|
||||
}
|
||||
// `bool & ~AlwaysTruthy` -> `bool & Literal[False]`
|
||||
// `bool & ~Literal[True]` -> `bool & Literal[False]`
|
||||
Type::AlwaysTruthy | Type::BooleanLiteral(true) if contains_bool() => {
|
||||
self.add_positive(db, Type::BooleanLiteral(false));
|
||||
}
|
||||
// `LiteralString & ~AlwaysTruthy` -> `LiteralString & Literal[""]`
|
||||
Type::AlwaysTruthy if self.positive.contains(&Type::LiteralString) => {
|
||||
self.add_positive(db, Type::string_literal(db, ""));
|
||||
}
|
||||
// `bool & ~AlwaysFalsy` -> `bool & Literal[True]`
|
||||
// `bool & ~Literal[False]` -> `bool & Literal[True]`
|
||||
Type::AlwaysFalsy | Type::BooleanLiteral(false) if contains_bool() => {
|
||||
self.add_positive(db, Type::BooleanLiteral(true));
|
||||
}
|
||||
// `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]`
|
||||
Type::AlwaysFalsy if self.positive.contains(&Type::LiteralString) => {
|
||||
self.add_negative(db, Type::string_literal(db, ""));
|
||||
// bool & ~Literal[True] = Literal[False]
|
||||
// bool & ~AlwaysTruthy = Literal[False]
|
||||
Type::BooleanLiteral(_) | Type::AlwaysFalsy | Type::AlwaysTruthy
|
||||
if self.positive.contains(&KnownClass::Bool.to_instance(db)) =>
|
||||
{
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::BooleanLiteral(
|
||||
new_negative.bool(db) != Truthiness::AlwaysTrue,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
@@ -457,8 +370,8 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for index in to_remove.into_iter().rev() {
|
||||
self.negative.swap_remove_index(index);
|
||||
for index in to_remove.iter().rev() {
|
||||
self.negative.swap_remove_index(*index);
|
||||
}
|
||||
|
||||
for existing_positive in &self.positive {
|
||||
|
||||
@@ -51,7 +51,7 @@ pub(super) enum CallOutcome<'db> {
|
||||
}
|
||||
|
||||
impl<'db> CallOutcome<'db> {
|
||||
/// Create a new `CallOutcome::Callable` with given binding.
|
||||
/// Create a new `CallOutcome::Callable` with given return type.
|
||||
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::Callable { binding }
|
||||
}
|
||||
@@ -80,7 +80,7 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::AssertType` with given asserted and return types.
|
||||
/// Create a new `CallOutcome::AssertType` with given revealed and return types.
|
||||
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
@@ -280,7 +280,7 @@ impl<'db> CallOutcome<'db> {
|
||||
}),
|
||||
}
|
||||
}
|
||||
Self::StaticAssertionError {
|
||||
CallOutcome::StaticAssertionError {
|
||||
binding,
|
||||
error_kind,
|
||||
} => {
|
||||
@@ -325,7 +325,7 @@ impl<'db> CallOutcome<'db> {
|
||||
|
||||
Ok(Type::unknown())
|
||||
}
|
||||
Self::AssertType {
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
} => {
|
||||
|
||||
@@ -84,7 +84,7 @@ pub(crate) fn bind_call<'db>(
|
||||
}
|
||||
}
|
||||
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
|
||||
if parameter.is_variadic() || parameter.is_keyword_variadic() {
|
||||
if parameter.is_variadic() {
|
||||
let union = UnionType::from_elements(db, [existing, *argument_ty]);
|
||||
parameter_tys[index].replace(union);
|
||||
} else {
|
||||
|
||||
@@ -105,9 +105,7 @@ impl<'db> ClassBase<'db> {
|
||||
| KnownInstanceType::Optional
|
||||
| KnownInstanceType::Not
|
||||
| KnownInstanceType::Intersection
|
||||
| KnownInstanceType::TypeOf
|
||||
| KnownInstanceType::AlwaysTruthy
|
||||
| KnownInstanceType::AlwaysFalsy => None,
|
||||
| KnownInstanceType::TypeOf => None,
|
||||
KnownInstanceType::Unknown => Some(Self::unknown()),
|
||||
KnownInstanceType::Any => Some(Self::any()),
|
||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::context::InferContext;
|
||||
use crate::declare_lint;
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::string_annotation::{
|
||||
@@ -6,8 +7,7 @@ use crate::types::string_annotation::{
|
||||
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
RAW_STRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use crate::types::{ClassLiteralType, KnownInstanceType, Type};
|
||||
use crate::{declare_lint, Db};
|
||||
use crate::types::{ClassLiteralType, Type};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
@@ -59,7 +59,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&UNSUPPORTED_OPERATOR);
|
||||
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
||||
registry.register_lint(&STATIC_ASSERT_ERROR);
|
||||
registry.register_lint(&INVALID_ATTRIBUTE_ACCESS);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -751,25 +750,6 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Makes sure that instance attribute accesses are valid.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// class C:
|
||||
/// var: ClassVar[int] = 1
|
||||
///
|
||||
/// C.var = 3 # okay
|
||||
/// C().var = 3 # error: Cannot assign to class variable
|
||||
/// ```
|
||||
pub(crate) static INVALID_ATTRIBUTE_ACCESS = {
|
||||
summary: "Invalid attribute access",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
pub(crate) id: DiagnosticId,
|
||||
@@ -1080,18 +1060,3 @@ pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node:
|
||||
format_args!("Class base has incompatible `__slots__`"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_arguments_to_annotated<'db>(
|
||||
db: &'db dyn Db,
|
||||
context: &InferContext<'db>,
|
||||
subscript: &ast::ExprSubscript,
|
||||
) {
|
||||
context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript.into(),
|
||||
format_args!(
|
||||
"Special form `{}` expected at least 2 arguments (one type and at least one metadata element)",
|
||||
KnownInstanceType::Annotated.repr(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,10 +51,9 @@ use crate::semantic_index::SemanticIndex;
|
||||
use crate::stdlib::builtins_module_scope;
|
||||
use crate::types::call::{Argument, CallArguments};
|
||||
use crate::types::diagnostic::{
|
||||
report_invalid_arguments_to_annotated, report_invalid_assignment, report_unresolved_module,
|
||||
TypeCheckDiagnostics, CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO,
|
||||
DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
report_invalid_assignment, report_unresolved_module, TypeCheckDiagnostics, CALL_NON_CALLABLE,
|
||||
CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE,
|
||||
INVALID_CONTEXT_MANAGER, INVALID_DECLARATION, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
|
||||
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
@@ -62,13 +61,12 @@ use crate::types::diagnostic::{
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
todo_type, typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType,
|
||||
DynamicType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType,
|
||||
IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate,
|
||||
MetaclassErrorKind, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness,
|
||||
TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type,
|
||||
typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType,
|
||||
FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome,
|
||||
KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind,
|
||||
SliceLiteralType, SubclassOfType, Symbol, Truthiness, TupleType, Type, TypeAliasType,
|
||||
TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
@@ -115,7 +113,7 @@ fn infer_definition_types_cycle_recovery<'db>(
|
||||
let mut inference = TypeInference::empty(input.scope(db));
|
||||
let category = input.category(db);
|
||||
if category.is_declaration() {
|
||||
inference.declarations.insert(input, Type::unknown().into());
|
||||
inference.declarations.insert(input, Type::unknown());
|
||||
}
|
||||
if category.is_binding() {
|
||||
inference.bindings.insert(input, Type::unknown());
|
||||
@@ -243,8 +241,8 @@ pub(crate) struct TypeInference<'db> {
|
||||
/// The types of every binding in this region.
|
||||
bindings: FxHashMap<Definition<'db>, Type<'db>>,
|
||||
|
||||
/// The types and type qualifiers of every declaration in this region.
|
||||
declarations: FxHashMap<Definition<'db>, TypeAndQualifiers<'db>>,
|
||||
/// The types of every declaration in this region.
|
||||
declarations: FxHashMap<Definition<'db>, Type<'db>>,
|
||||
|
||||
/// The definitions that are deferred.
|
||||
deferred: FxHashSet<Definition<'db>>,
|
||||
@@ -283,7 +281,7 @@ impl<'db> TypeInference<'db> {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn declaration_ty(&self, definition: Definition<'db>) -> TypeAndQualifiers<'db> {
|
||||
pub(crate) fn declaration_ty(&self, definition: Definition<'db>) -> Type<'db> {
|
||||
self.declarations[&definition]
|
||||
}
|
||||
|
||||
@@ -320,7 +318,7 @@ enum DeclaredAndInferredType<'db> {
|
||||
AreTheSame(Type<'db>),
|
||||
/// Declared and inferred types might be different, we need to check assignability.
|
||||
MightBeDifferent {
|
||||
declared_ty: TypeAndQualifiers<'db>,
|
||||
declared_ty: Type<'db>,
|
||||
inferred_ty: Type<'db>,
|
||||
},
|
||||
}
|
||||
@@ -565,9 +563,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.filter_map(|(definition, ty)| {
|
||||
// Filter out class literals that result from imports
|
||||
if let DefinitionKind::Class(class) = definition.kind(self.db()) {
|
||||
ty.inner_ty()
|
||||
.into_class_literal()
|
||||
.map(|ty| (ty.class, class.node()))
|
||||
ty.into_class_literal().map(|ty| (ty.class, class.node()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -576,17 +572,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// Iterate through all class definitions in this scope.
|
||||
for (class, class_node) in class_definitions {
|
||||
// (1) Check that the class does not have a cyclic definition
|
||||
if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) {
|
||||
if inheritance_cycle.is_participant() {
|
||||
self.context.report_lint(
|
||||
&CYCLIC_CLASS_DEFINITION,
|
||||
class_node.into(),
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
if class.is_cyclically_defined(self.db()) {
|
||||
self.context.report_lint(
|
||||
&CYCLIC_CLASS_DEFINITION,
|
||||
class_node.into(),
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db()),
|
||||
class.name(self.db())
|
||||
),
|
||||
);
|
||||
// Attempting to determine the MRO of a class or if the class has a metaclass conflict
|
||||
// is impossible if the class is cyclically defined; there's nothing more to do here.
|
||||
continue;
|
||||
@@ -859,8 +854,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let use_def = self.index.use_def_map(binding.file_scope(self.db()));
|
||||
let declarations = use_def.declarations_at_binding(binding);
|
||||
let mut bound_ty = ty;
|
||||
let declared_ty = symbol_from_declarations(self.db(), declarations)
|
||||
.map(|SymbolAndQualifiers(s, _)| s.ignore_possibly_unbound().unwrap_or(Type::unknown()))
|
||||
let declared_ty = declarations_ty(self.db(), declarations)
|
||||
.map(|s| s.ignore_possibly_unbound().unwrap_or(Type::unknown()))
|
||||
.unwrap_or_else(|(ty, conflicting)| {
|
||||
// TODO point out the conflicting declarations in the diagnostic?
|
||||
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
|
||||
@@ -873,7 +868,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
conflicting.display(self.db())
|
||||
),
|
||||
);
|
||||
ty.inner_ty()
|
||||
ty
|
||||
});
|
||||
if !bound_ty.is_assignable_to(self.db(), declared_ty) {
|
||||
report_invalid_assignment(&self.context, node, declared_ty, bound_ty);
|
||||
@@ -884,20 +879,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.types.bindings.insert(binding, bound_ty);
|
||||
}
|
||||
|
||||
fn add_declaration(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
declaration: Definition<'db>,
|
||||
ty: TypeAndQualifiers<'db>,
|
||||
) {
|
||||
fn add_declaration(&mut self, node: AnyNodeRef, declaration: Definition<'db>, ty: Type<'db>) {
|
||||
debug_assert!(declaration.is_declaration(self.db()));
|
||||
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
|
||||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
let inferred_ty = symbol_from_bindings(self.db(), prior_bindings)
|
||||
let inferred_ty = bindings_ty(self.db(), prior_bindings)
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Never);
|
||||
let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_ty()) {
|
||||
let ty = if inferred_ty.is_assignable_to(self.db(), ty) {
|
||||
ty
|
||||
} else {
|
||||
self.context.report_lint(
|
||||
@@ -905,11 +895,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
node,
|
||||
format_args!(
|
||||
"Cannot declare type `{}` for inferred type `{}`",
|
||||
ty.inner_ty().display(self.db()),
|
||||
ty.display(self.db()),
|
||||
inferred_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
Type::unknown().into()
|
||||
Type::unknown()
|
||||
};
|
||||
self.types.declarations.insert(declaration, ty);
|
||||
}
|
||||
@@ -923,28 +913,23 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
debug_assert!(definition.is_binding(self.db()));
|
||||
debug_assert!(definition.is_declaration(self.db()));
|
||||
|
||||
let (declared_ty, inferred_ty) = match *declared_and_inferred_ty {
|
||||
DeclaredAndInferredType::AreTheSame(ty) => (ty.into(), ty),
|
||||
let (declared_ty, inferred_ty) = match declared_and_inferred_ty {
|
||||
DeclaredAndInferredType::AreTheSame(ty) => (ty, ty),
|
||||
DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty,
|
||||
inferred_ty,
|
||||
} => {
|
||||
if inferred_ty.is_assignable_to(self.db(), declared_ty.inner_ty()) {
|
||||
if inferred_ty.is_assignable_to(self.db(), *declared_ty) {
|
||||
(declared_ty, inferred_ty)
|
||||
} else {
|
||||
report_invalid_assignment(
|
||||
&self.context,
|
||||
node,
|
||||
declared_ty.inner_ty(),
|
||||
inferred_ty,
|
||||
);
|
||||
report_invalid_assignment(&self.context, node, *declared_ty, *inferred_ty);
|
||||
// if the assignment is invalid, fall back to assuming the annotation is correct
|
||||
(declared_ty, declared_ty.inner_ty())
|
||||
(declared_ty, declared_ty)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.types.declarations.insert(definition, declared_ty);
|
||||
self.types.bindings.insert(definition, inferred_ty);
|
||||
self.types.declarations.insert(definition, *declared_ty);
|
||||
self.types.bindings.insert(definition, *inferred_ty);
|
||||
}
|
||||
|
||||
fn add_unknown_declaration_with_binding(
|
||||
@@ -1235,7 +1220,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let declared_and_inferred_ty = if let Some(default_ty) = default_ty {
|
||||
if default_ty.is_assignable_to(self.db(), declared_ty) {
|
||||
DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty: declared_ty.into(),
|
||||
declared_ty,
|
||||
inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]),
|
||||
}
|
||||
} else if self.in_stub()
|
||||
@@ -2081,7 +2066,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
|
||||
// Handle various singletons.
|
||||
if let Type::Instance(InstanceType { class }) = declared_ty.inner_ty() {
|
||||
if let Type::Instance(InstanceType { class }) = declared_ty {
|
||||
if class.is_known(self.db(), KnownClass::SpecialForm) {
|
||||
if let Some(name_expr) = target.as_name_expr() {
|
||||
if let Some(known_instance) = KnownInstanceType::try_from_file_and_name(
|
||||
@@ -2089,7 +2074,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.file(),
|
||||
&name_expr.id,
|
||||
) {
|
||||
declared_ty.inner = Type::KnownInstance(known_instance);
|
||||
declared_ty = Type::KnownInstance(known_instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2098,7 +2083,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if let Some(value) = value.as_deref() {
|
||||
let inferred_ty = self.infer_expression(value);
|
||||
let inferred_ty = if self.in_stub() && value.is_ellipsis_literal_expr() {
|
||||
declared_ty.inner_ty()
|
||||
declared_ty
|
||||
} else {
|
||||
inferred_ty
|
||||
};
|
||||
@@ -3310,9 +3295,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let use_def = self.index.use_def_map(file_scope_id);
|
||||
|
||||
// If we're inferring types of deferred expressions, always treat them as public symbols
|
||||
let inferred = if self.is_deferred() {
|
||||
let bindings_ty = if self.is_deferred() {
|
||||
if let Some(symbol) = self.index.symbol_table(file_scope_id).symbol_id_by_name(id) {
|
||||
symbol_from_bindings(self.db(), use_def.public_bindings(symbol))
|
||||
bindings_ty(self.db(), use_def.public_bindings(symbol))
|
||||
} else {
|
||||
assert!(
|
||||
self.deferred_state.in_string_annotation(),
|
||||
@@ -3322,10 +3307,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
} else {
|
||||
let use_id = name.scoped_use_id(self.db(), self.scope());
|
||||
symbol_from_bindings(self.db(), use_def.bindings_at_use(use_id))
|
||||
bindings_ty(self.db(), use_def.bindings_at_use(use_id))
|
||||
};
|
||||
|
||||
if let Symbol::Type(ty, Boundness::Bound) = inferred {
|
||||
if let Symbol::Type(ty, Boundness::Bound) = bindings_ty {
|
||||
ty
|
||||
} else {
|
||||
match self.lookup_name(name) {
|
||||
@@ -3334,12 +3319,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
report_possibly_unresolved_reference(&self.context, name);
|
||||
}
|
||||
|
||||
inferred
|
||||
bindings_ty
|
||||
.ignore_possibly_unbound()
|
||||
.map(|ty| UnionType::from_elements(self.db(), [ty, looked_up_ty]))
|
||||
.unwrap_or(looked_up_ty)
|
||||
}
|
||||
Symbol::Unbound => match inferred {
|
||||
Symbol::Unbound => match bindings_ty {
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
report_possibly_unresolved_reference(&self.context, name);
|
||||
ty
|
||||
@@ -3406,33 +3391,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
||||
let ast::ExprAttribute {
|
||||
value,
|
||||
attr,
|
||||
attr: _,
|
||||
range: _,
|
||||
ctx,
|
||||
} = attribute;
|
||||
|
||||
match ctx {
|
||||
ExprContext::Load => self.infer_attribute_load(attribute),
|
||||
ExprContext::Store => {
|
||||
let value_ty = self.infer_expression(value);
|
||||
|
||||
if let Type::Instance(instance) = value_ty {
|
||||
let instance_member = instance.class.instance_member(self.db(), attr);
|
||||
if instance_member.is_class_var() {
|
||||
self.context.report_lint(
|
||||
&INVALID_ATTRIBUTE_ACCESS,
|
||||
attribute.into(),
|
||||
format_args!(
|
||||
"Cannot assign to ClassVar `{attr}` from an instance of type `{ty}`",
|
||||
ty = value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Type::Never
|
||||
}
|
||||
ExprContext::Del => {
|
||||
ExprContext::Store | ExprContext::Del => {
|
||||
self.infer_expression(value);
|
||||
Type::Never
|
||||
}
|
||||
@@ -4573,7 +4539,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
return dunder_getitem_method
|
||||
.call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
|
||||
.return_ty_result(&self.context, value_node.into())
|
||||
.return_ty_result( &self.context, value_node.into())
|
||||
.unwrap_or_else(|err| {
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
@@ -4732,7 +4698,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
&mut self,
|
||||
annotation: &ast::Expr,
|
||||
deferred_state: DeferredExpressionState,
|
||||
) -> TypeAndQualifiers<'db> {
|
||||
) -> Type<'db> {
|
||||
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, deferred_state);
|
||||
let annotation_ty = self.infer_annotation_expression_impl(annotation);
|
||||
self.deferred_state = previous_deferred_state;
|
||||
@@ -4747,24 +4713,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
&mut self,
|
||||
annotation: Option<&ast::Expr>,
|
||||
deferred_state: DeferredExpressionState,
|
||||
) -> Option<TypeAndQualifiers<'db>> {
|
||||
) -> Option<Type<'db>> {
|
||||
annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state))
|
||||
}
|
||||
|
||||
/// Implementation of [`infer_annotation_expression`].
|
||||
///
|
||||
/// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression
|
||||
fn infer_annotation_expression_impl(
|
||||
&mut self,
|
||||
annotation: &ast::Expr,
|
||||
) -> TypeAndQualifiers<'db> {
|
||||
fn infer_annotation_expression_impl(&mut self, annotation: &ast::Expr) -> Type<'db> {
|
||||
// https://typing.readthedocs.io/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression
|
||||
let annotation_ty = match annotation {
|
||||
// String annotations: https://typing.readthedocs.io/en/latest/spec/annotations.html#string-annotations
|
||||
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
|
||||
|
||||
// Annotation expressions also get special handling for `*args` and `**kwargs`.
|
||||
ast::Expr::Starred(starred) => self.infer_starred_expression(starred).into(),
|
||||
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
|
||||
|
||||
ast::Expr::BytesLiteral(bytes) => {
|
||||
self.context.report_lint(
|
||||
@@ -4772,7 +4735,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
bytes.into(),
|
||||
format_args!("Type expressions cannot use bytes literal"),
|
||||
);
|
||||
Type::unknown().into()
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::FString(fstring) => {
|
||||
@@ -4782,126 +4745,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
format_args!("Type expressions cannot use f-strings"),
|
||||
);
|
||||
self.infer_fstring_expression(fstring);
|
||||
Type::unknown().into()
|
||||
}
|
||||
|
||||
ast::Expr::Name(name) => match name.ctx {
|
||||
ast::ExprContext::Load => {
|
||||
let name_expr_ty = self.infer_name_expression(name);
|
||||
match name_expr_ty {
|
||||
Type::KnownInstance(KnownInstanceType::ClassVar) => {
|
||||
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::Final) => {
|
||||
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
|
||||
}
|
||||
_ => name_expr_ty
|
||||
.in_type_expression(self.db())
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(&self.context, annotation)
|
||||
})
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
ast::ExprContext::Invalid => Type::unknown().into(),
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!().into(),
|
||||
},
|
||||
|
||||
ast::Expr::Subscript(subscript @ ast::ExprSubscript { value, slice, .. }) => {
|
||||
let value_ty = self.infer_expression(value);
|
||||
|
||||
let slice = &**slice;
|
||||
|
||||
match value_ty {
|
||||
Type::KnownInstance(KnownInstanceType::Annotated) => {
|
||||
// This branch is similar to the corresponding branch in `infer_parameterized_known_instance_type_expression`, but
|
||||
// `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly
|
||||
// differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`).
|
||||
if let ast::Expr::Tuple(ast::ExprTuple {
|
||||
elts: arguments, ..
|
||||
}) = slice
|
||||
{
|
||||
if arguments.len() < 2 {
|
||||
report_invalid_arguments_to_annotated(
|
||||
self.db(),
|
||||
&self.context,
|
||||
subscript,
|
||||
);
|
||||
}
|
||||
|
||||
if let [inner_annotation, metadata @ ..] = &arguments[..] {
|
||||
for element in metadata {
|
||||
self.infer_expression(element);
|
||||
}
|
||||
|
||||
let inner_annotation_ty =
|
||||
self.infer_annotation_expression_impl(inner_annotation);
|
||||
|
||||
self.store_expression_type(slice, inner_annotation_ty.inner_ty());
|
||||
inner_annotation_ty
|
||||
} else {
|
||||
self.infer_type_expression(slice);
|
||||
Type::unknown().into()
|
||||
}
|
||||
} else {
|
||||
report_invalid_arguments_to_annotated(
|
||||
self.db(),
|
||||
&self.context,
|
||||
subscript,
|
||||
);
|
||||
self.infer_annotation_expression_impl(slice)
|
||||
}
|
||||
}
|
||||
Type::KnownInstance(
|
||||
known_instance @ (KnownInstanceType::ClassVar | KnownInstanceType::Final),
|
||||
) => match slice {
|
||||
ast::Expr::Tuple(..) => {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript.into(),
|
||||
format_args!(
|
||||
"Type qualifier `{type_qualifier}` expects exactly one type parameter",
|
||||
type_qualifier = known_instance.repr(self.db()),
|
||||
),
|
||||
);
|
||||
Type::unknown().into()
|
||||
}
|
||||
_ => {
|
||||
let mut type_and_qualifiers =
|
||||
self.infer_annotation_expression_impl(slice);
|
||||
match known_instance {
|
||||
KnownInstanceType::ClassVar => {
|
||||
type_and_qualifiers.add_qualifier(TypeQualifiers::CLASS_VAR);
|
||||
}
|
||||
KnownInstanceType::Final => {
|
||||
type_and_qualifiers.add_qualifier(TypeQualifiers::FINAL);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
type_and_qualifiers
|
||||
}
|
||||
},
|
||||
_ => self
|
||||
.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
|
||||
.into(),
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
// All other annotation expressions are (possibly) valid type expressions, so handle
|
||||
// them there instead.
|
||||
type_expr => self.infer_type_expression_no_store(type_expr).into(),
|
||||
type_expr => self.infer_type_expression_no_store(type_expr),
|
||||
};
|
||||
|
||||
self.store_expression_type(annotation, annotation_ty.inner_ty());
|
||||
self.store_expression_type(annotation, annotation_ty);
|
||||
|
||||
annotation_ty
|
||||
}
|
||||
|
||||
/// Infer the type of a string annotation expression.
|
||||
fn infer_string_annotation_expression(
|
||||
&mut self,
|
||||
string: &ast::ExprStringLiteral,
|
||||
) -> TypeAndQualifiers<'db> {
|
||||
fn infer_string_annotation_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> {
|
||||
match parse_string_annotation(&self.context, string) {
|
||||
Some(parsed) => {
|
||||
// String annotations are always evaluated in the deferred context.
|
||||
@@ -4910,7 +4768,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
DeferredExpressionState::InStringAnnotation,
|
||||
)
|
||||
}
|
||||
None => Type::unknown().into(),
|
||||
None => Type::unknown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4997,7 +4855,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
let value_ty = self.infer_expression(value);
|
||||
|
||||
self.infer_subscript_type_expression_no_store(subscript, slice, value_ty)
|
||||
match value_ty {
|
||||
Type::ClassLiteral(class_literal_ty) => {
|
||||
match class_literal_ty.class.known(self.db()) {
|
||||
Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice),
|
||||
Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice),
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
}
|
||||
}
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
}
|
||||
}
|
||||
|
||||
ast::Expr::BinOp(binary) => {
|
||||
@@ -5113,22 +4980,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_subscript_type_expression_no_store(
|
||||
&mut self,
|
||||
subscript: &ast::ExprSubscript,
|
||||
slice: &ast::Expr,
|
||||
value_ty: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
match value_ty {
|
||||
Type::ClassLiteral(class_literal_ty) => match class_literal_ty.class.known(self.db()) {
|
||||
Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice),
|
||||
Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice),
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
},
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the type of a string type expression.
|
||||
fn infer_string_type_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> {
|
||||
match parse_string_annotation(&self.context, string) {
|
||||
@@ -5312,11 +5163,22 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let arguments_slice = &*subscript.slice;
|
||||
match known_instance {
|
||||
KnownInstanceType::Annotated => {
|
||||
let report_invalid_arguments = || {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript.into(),
|
||||
format_args!(
|
||||
"Special form `{}` expected at least 2 arguments (one type and at least one metadata element)",
|
||||
known_instance.repr(self.db())
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
let ast::Expr::Tuple(ast::ExprTuple {
|
||||
elts: arguments, ..
|
||||
}) = arguments_slice
|
||||
else {
|
||||
report_invalid_arguments_to_annotated(self.db(), &self.context, subscript);
|
||||
report_invalid_arguments();
|
||||
|
||||
// `Annotated[]` with less than two arguments is an error at runtime.
|
||||
// However, we still treat `Annotated[T]` as `T` here for the purpose of
|
||||
@@ -5326,7 +5188,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
};
|
||||
|
||||
if arguments.len() < 2 {
|
||||
report_invalid_arguments_to_annotated(self.db(), &self.context, subscript);
|
||||
report_invalid_arguments();
|
||||
}
|
||||
|
||||
let [type_expr, metadata @ ..] = &arguments[..] else {
|
||||
@@ -5483,16 +5345,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`NotRequired[]` type qualifier")
|
||||
}
|
||||
KnownInstanceType::ClassVar | KnownInstanceType::Final => {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript.into(),
|
||||
format_args!(
|
||||
"Type qualifier `{}` is not allowed in type expressions (only in annotation expressions)",
|
||||
known_instance.repr(self.db())
|
||||
),
|
||||
);
|
||||
self.infer_type_expression(arguments_slice)
|
||||
KnownInstanceType::ClassVar => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`ClassVar[]` type qualifier")
|
||||
}
|
||||
KnownInstanceType::Final => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`Final[]` type qualifier")
|
||||
}
|
||||
KnownInstanceType::Required => {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
@@ -5514,11 +5373,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_type_expression(arguments_slice);
|
||||
todo_type!("`Unpack[]` special form")
|
||||
}
|
||||
KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Any
|
||||
| KnownInstanceType::AlwaysTruthy
|
||||
| KnownInstanceType::AlwaysFalsy => {
|
||||
KnownInstanceType::NoReturn | KnownInstanceType::Never | KnownInstanceType::Any => {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
subscript.into(),
|
||||
|
||||
@@ -42,7 +42,7 @@ impl<'db> Mro<'db> {
|
||||
fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result<Self, MroErrorKind<'db>> {
|
||||
let class_bases = class.explicit_bases(db);
|
||||
|
||||
if !class_bases.is_empty() && class.inheritance_cycle(db).is_some() {
|
||||
if !class_bases.is_empty() && class.is_cyclically_defined(db) {
|
||||
// We emit errors for cyclically defined classes elsewhere.
|
||||
// It's important that we don't even try to infer the MRO for a cyclically defined class,
|
||||
// or we'll end up in an infinite loop.
|
||||
|
||||
@@ -26,107 +26,11 @@
|
||||
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
|
||||
use super::tests::Ty;
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{
|
||||
builtins_symbol, known_module_symbol, IntersectionBuilder, KnownClass, KnownInstanceType,
|
||||
SubclassOfType, TupleType, Type, UnionType,
|
||||
};
|
||||
use crate::KnownModule;
|
||||
use crate::types::KnownClass;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
|
||||
/// A test representation of a type that can be transformed unambiguously into a real Type,
|
||||
/// given a db.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Ty {
|
||||
Never,
|
||||
Unknown,
|
||||
None,
|
||||
Any,
|
||||
IntLiteral(i64),
|
||||
BooleanLiteral(bool),
|
||||
StringLiteral(&'static str),
|
||||
LiteralString,
|
||||
BytesLiteral(&'static str),
|
||||
// BuiltinInstance("str") corresponds to an instance of the builtin `str` class
|
||||
BuiltinInstance(&'static str),
|
||||
/// Members of the `abc` stdlib module
|
||||
AbcInstance(&'static str),
|
||||
AbcClassLiteral(&'static str),
|
||||
TypingLiteral,
|
||||
// BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself
|
||||
BuiltinClassLiteral(&'static str),
|
||||
KnownClassInstance(KnownClass),
|
||||
Union(Vec<Ty>),
|
||||
Intersection {
|
||||
pos: Vec<Ty>,
|
||||
neg: Vec<Ty>,
|
||||
},
|
||||
Tuple(Vec<Ty>),
|
||||
SubclassOfAny,
|
||||
SubclassOfBuiltinClass(&'static str),
|
||||
SubclassOfAbcClass(&'static str),
|
||||
AlwaysTruthy,
|
||||
AlwaysFalsy,
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> {
|
||||
match self {
|
||||
Ty::Never => Type::Never,
|
||||
Ty::Unknown => Type::unknown(),
|
||||
Ty::None => Type::none(db),
|
||||
Ty::Any => Type::any(),
|
||||
Ty::IntLiteral(n) => Type::IntLiteral(n),
|
||||
Ty::StringLiteral(s) => Type::string_literal(db, s),
|
||||
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
|
||||
Ty::LiteralString => Type::LiteralString,
|
||||
Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db),
|
||||
Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s)
|
||||
.expect_type()
|
||||
.to_instance(db),
|
||||
Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s).expect_type(),
|
||||
Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal),
|
||||
Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).expect_type(),
|
||||
Ty::KnownClassInstance(known_class) => known_class.to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
}
|
||||
Ty::Intersection { pos, neg } => {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
for p in pos {
|
||||
builder = builder.add_positive(p.into_type(db));
|
||||
}
|
||||
for n in neg {
|
||||
builder = builder.add_negative(n.into_type(db));
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
Ty::Tuple(tys) => {
|
||||
let elements = tys.into_iter().map(|ty| ty.into_type(db));
|
||||
TupleType::from_elements(db, elements)
|
||||
}
|
||||
Ty::SubclassOfAny => SubclassOfType::subclass_of_any(),
|
||||
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
|
||||
db,
|
||||
builtins_symbol(db, s)
|
||||
.expect_type()
|
||||
.expect_class_literal()
|
||||
.class,
|
||||
),
|
||||
Ty::SubclassOfAbcClass(s) => SubclassOfType::from(
|
||||
db,
|
||||
known_module_symbol(db, KnownModule::Abc, s)
|
||||
.expect_type()
|
||||
.expect_class_literal()
|
||||
.class,
|
||||
),
|
||||
Ty::AlwaysTruthy => Type::AlwaysTruthy,
|
||||
Ty::AlwaysFalsy => Type::AlwaysFalsy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
||||
// We could select a random integer here, but this would make it much less
|
||||
// likely to explore interesting edge cases:
|
||||
@@ -301,7 +205,7 @@ macro_rules! type_property_test {
|
||||
($test_name:ident, $db:ident, forall types $($types:ident),+ . $property:expr) => {
|
||||
#[quickcheck_macros::quickcheck]
|
||||
#[ignore]
|
||||
fn $test_name($($types: super::Ty),+) -> bool {
|
||||
fn $test_name($($types: crate::types::tests::Ty),+) -> bool {
|
||||
let db_cached = super::get_cached_db();
|
||||
let $db = &*db_cached;
|
||||
$(let $types = $types.into_type($db);)+
|
||||
@@ -315,48 +219,16 @@ macro_rules! type_property_test {
|
||||
};
|
||||
}
|
||||
|
||||
fn intersection<'db>(db: &'db TestDb, tys: impl IntoIterator<Item = Type<'db>>) -> Type<'db> {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
for ty in tys {
|
||||
builder = builder.add_positive(ty);
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
fn union<'db>(db: &'db TestDb, tys: impl IntoIterator<Item = Type<'db>>) -> Type<'db> {
|
||||
UnionType::from_elements(db, tys)
|
||||
}
|
||||
|
||||
mod stable {
|
||||
use super::union;
|
||||
use crate::types::{KnownClass, Type};
|
||||
|
||||
// Reflexivity: `T` is equivalent to itself.
|
||||
// `T` is equivalent to itself.
|
||||
type_property_test!(
|
||||
equivalent_to_is_reflexive, db,
|
||||
forall types t. t.is_fully_static(db) => t.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`.
|
||||
// Note that this (trivially) holds true for gradual types as well.
|
||||
type_property_test!(
|
||||
equivalent_to_is_symmetric, db,
|
||||
forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s)
|
||||
);
|
||||
|
||||
// Transitivity: If `S` is equivalent to `T` and `T` is equivalent to `U`, then `S` must be equivalent to `U`.
|
||||
type_property_test!(
|
||||
equivalent_to_is_transitive, db,
|
||||
forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u)
|
||||
);
|
||||
|
||||
// Symmetry: If `S` is gradual equivalent to `T`, `T` is gradual equivalent to `S`.
|
||||
type_property_test!(
|
||||
gradual_equivalent_to_is_symmetric, db,
|
||||
forall types s, t. s.is_gradual_equivalent_to(db, t) => t.is_gradual_equivalent_to(db, s)
|
||||
);
|
||||
|
||||
// A fully static type `T` is a subtype of itself.
|
||||
// `T` is a subtype of itself.
|
||||
type_property_test!(
|
||||
subtype_of_is_reflexive, db,
|
||||
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, t)
|
||||
@@ -368,12 +240,6 @@ mod stable {
|
||||
forall types s, t, u. s.is_subtype_of(db, t) && t.is_subtype_of(db, u) => s.is_subtype_of(db, u)
|
||||
);
|
||||
|
||||
// `S <: T` and `T <: S` implies that `S` is equivalent to `T`.
|
||||
type_property_test!(
|
||||
subtype_of_is_antisymmetric, db,
|
||||
forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// `T` is not disjoint from itself, unless `T` is `Never`.
|
||||
type_property_test!(
|
||||
disjoint_from_is_irreflexive, db,
|
||||
@@ -439,28 +305,6 @@ mod stable {
|
||||
never_subtype_of_every_fully_static_type, db,
|
||||
forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// For any two fully static types, each type in the pair must be a subtype of their union.
|
||||
type_property_test!(
|
||||
all_fully_static_type_pairs_are_subtype_of_their_union, db,
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && t.is_fully_static(db)
|
||||
=> s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t]))
|
||||
);
|
||||
|
||||
// A fully static type does not have any materializations.
|
||||
// Thus, two equivalent (fully static) types are also gradual equivalent.
|
||||
type_property_test!(
|
||||
two_equivalent_types_are_also_gradual_equivalent, db,
|
||||
forall types s, t. s.is_equivalent_to(db, t) => s.is_gradual_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// Two gradual equivalent fully static types are also equivalent.
|
||||
type_property_test!(
|
||||
two_gradual_equivalent_fully_static_types_are_also_equivalent, db,
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && s.is_gradual_equivalent_to(db, t) => s.is_equivalent_to(db, t)
|
||||
);
|
||||
}
|
||||
|
||||
/// This module contains property tests that currently lead to many false positives.
|
||||
@@ -471,9 +315,10 @@ mod stable {
|
||||
/// tests to the `stable` section. In the meantime, it can still be useful to run these
|
||||
/// tests (using [`types::property_tests::flaky`]), to see if there are any new obvious bugs.
|
||||
mod flaky {
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{intersection, union};
|
||||
use crate::{
|
||||
db::tests::TestDb,
|
||||
types::{IntersectionBuilder, Type},
|
||||
};
|
||||
|
||||
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
|
||||
// `T` can be assigned to itself.
|
||||
@@ -482,80 +327,29 @@ mod flaky {
|
||||
forall types t. t.is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
|
||||
// An intersection of two types should be assignable to both of them
|
||||
fn intersection<'db>(db: &'db TestDb, s: Type<'db>, t: Type<'db>) -> Type<'db> {
|
||||
IntersectionBuilder::new(db)
|
||||
.add_positive(s)
|
||||
.add_positive(t)
|
||||
.build()
|
||||
}
|
||||
|
||||
type_property_test!(
|
||||
intersection_assignable_to_both, db,
|
||||
forall types s, t. intersection(db, s, t).is_assignable_to(db, s) && intersection(db, s, t).is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// `S <: T` and `T <: S` implies that `S` is equivalent to `T`.
|
||||
type_property_test!(
|
||||
subtype_of_is_antisymmetric, db,
|
||||
forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// Negating `T` twice is equivalent to `T`.
|
||||
type_property_test!(
|
||||
double_negation_is_identity, db,
|
||||
forall types t. t.negate(db).negate(db).is_equivalent_to(db, t)
|
||||
);
|
||||
|
||||
// ~T should be disjoint from T
|
||||
type_property_test!(
|
||||
negation_is_disjoint, db,
|
||||
forall types t. t.is_fully_static(db) => t.negate(db).is_disjoint_from(db, t)
|
||||
);
|
||||
|
||||
// If `S <: T`, then `~T <: ~S`.
|
||||
type_property_test!(
|
||||
negation_reverses_subtype_order, db,
|
||||
forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db))
|
||||
);
|
||||
|
||||
// For two fully static types, their intersection must be a subtype of each type in the pair.
|
||||
type_property_test!(
|
||||
all_fully_static_type_pairs_are_supertypes_of_their_intersection, db,
|
||||
forall types s, t.
|
||||
s.is_fully_static(db) && t.is_fully_static(db)
|
||||
=> intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t)
|
||||
);
|
||||
|
||||
// And for non-fully-static types, the intersection of a pair of types
|
||||
// should be assignable to both types of the pair.
|
||||
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
|
||||
type_property_test!(
|
||||
all_type_pairs_can_be_assigned_from_their_intersection, db,
|
||||
forall types s, t. intersection(db, [s, t]).is_assignable_to(db, s) && intersection(db, [s, t]).is_assignable_to(db, t)
|
||||
);
|
||||
|
||||
// For *any* pair of types, whether fully static or not,
|
||||
// each of the pair should be assignable to the union of the two.
|
||||
type_property_test!(
|
||||
all_type_pairs_are_assignable_to_their_union, db,
|
||||
forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t]))
|
||||
);
|
||||
|
||||
// Equal element sets of intersections implies equivalence
|
||||
// flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513
|
||||
type_property_test!(
|
||||
intersection_equivalence_not_order_dependent, db,
|
||||
forall types s, t, u.
|
||||
s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db)
|
||||
=> [s, t, u]
|
||||
.into_iter()
|
||||
.permutations(3)
|
||||
.map(|trio_of_types| intersection(db, trio_of_types))
|
||||
.permutations(2)
|
||||
.all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, vec_of_intersections[1]))
|
||||
);
|
||||
|
||||
// Equal element sets of unions implies equivalence
|
||||
// flaky at laest in part because of https://github.com/astral-sh/ruff/issues/15513
|
||||
type_property_test!(
|
||||
union_equivalence_not_order_dependent, db,
|
||||
forall types s, t, u.
|
||||
s.is_fully_static(db) && t.is_fully_static(db) && u.is_fully_static(db)
|
||||
=> [s, t, u]
|
||||
.into_iter()
|
||||
.permutations(3)
|
||||
.map(|trio_of_types| union(db, trio_of_types))
|
||||
.permutations(2)
|
||||
.all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, vec_of_unions[1]))
|
||||
);
|
||||
|
||||
// `S | T` is always a supertype of `S`.
|
||||
// Thus, `S` is never disjoint from `S | T`.
|
||||
type_property_test!(
|
||||
constituent_members_of_union_is_not_disjoint_from_that_union, db,
|
||||
forall types s, t.
|
||||
!s.is_disjoint_from(db, union(db, [s, t])) && !t.is_disjoint_from(db, union(db, [s, t]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_python_ast::str::raw_contents;
|
||||
use ruff_python_ast::{self as ast, ModExpression};
|
||||
use ruff_python_parser::Parsed;
|
||||
use ruff_python_ast::{self as ast, ModExpression, StringFlags};
|
||||
use ruff_python_parser::{parse_expression_range, Parsed};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::declare_lint;
|
||||
@@ -153,7 +153,19 @@ pub(crate) fn parse_string_annotation(
|
||||
} else if raw_contents(node_text)
|
||||
.is_some_and(|raw_contents| raw_contents == string_literal.as_str())
|
||||
{
|
||||
match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) {
|
||||
let range_excluding_quotes = string_literal
|
||||
.range()
|
||||
.add_start(string_literal.flags.opener_len())
|
||||
.sub_end(string_literal.flags.closer_len());
|
||||
|
||||
// TODO: Support multiline strings like:
|
||||
// ```py
|
||||
// x: """
|
||||
// int
|
||||
// | float
|
||||
// """ = 1
|
||||
// ```
|
||||
match parse_expression_range(source.as_str(), range_excluding_quotes) {
|
||||
Ok(parsed) => return Some(parsed),
|
||||
Err(parse_error) => context.report_lint(
|
||||
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::{
|
||||
class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType,
|
||||
TodoType, Type,
|
||||
};
|
||||
|
||||
/// Return an [`Ordering`] that describes the canonical order in which two types should appear
|
||||
/// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them
|
||||
/// to be compared for equivalence.
|
||||
///
|
||||
/// Two unions with equal sets of elements will only compare equal if they have their element sets
|
||||
/// ordered the same way.
|
||||
///
|
||||
/// ## Why not just implement [`Ord`] on [`Type`]?
|
||||
///
|
||||
/// It would be fairly easy to slap `#[derive(PartialOrd, Ord)]` on [`Type`], and the ordering we
|
||||
/// create here is not user-facing. However, it doesn't really "make sense" for `Type` to implement
|
||||
/// [`Ord`] in terms of the semantics. There are many different ways in which you could plausibly
|
||||
/// sort a list of types; this is only one (somewhat arbitrary, at times) possible ordering.
|
||||
pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>) -> Ordering {
|
||||
if left == right {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
|
||||
match (left, right) {
|
||||
(Type::Never, _) => Ordering::Less,
|
||||
(_, Type::Never) => Ordering::Greater,
|
||||
|
||||
(Type::LiteralString, _) => Ordering::Less,
|
||||
(_, Type::LiteralString) => Ordering::Greater,
|
||||
|
||||
(Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left.cmp(right),
|
||||
(Type::BooleanLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::BooleanLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::IntLiteral(left), Type::IntLiteral(right)) => left.cmp(right),
|
||||
(Type::IntLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::IntLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::StringLiteral(left), Type::StringLiteral(right)) => left.cmp(right),
|
||||
(Type::StringLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::StringLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::BytesLiteral(left), Type::BytesLiteral(right)) => left.cmp(right),
|
||||
(Type::BytesLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::BytesLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::SliceLiteral(left), Type::SliceLiteral(right)) => left.cmp(right),
|
||||
(Type::SliceLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::SliceLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::FunctionLiteral(left), Type::FunctionLiteral(right)) => left.cmp(right),
|
||||
(Type::FunctionLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::FunctionLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::Tuple(left), Type::Tuple(right)) => left.cmp(right),
|
||||
(Type::Tuple(_), _) => Ordering::Less,
|
||||
(_, Type::Tuple(_)) => Ordering::Greater,
|
||||
|
||||
(Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => left.cmp(right),
|
||||
(Type::ModuleLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::ModuleLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(
|
||||
Type::ClassLiteral(ClassLiteralType { class: left }),
|
||||
Type::ClassLiteral(ClassLiteralType { class: right }),
|
||||
) => left.cmp(right),
|
||||
(Type::ClassLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::ClassLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(Type::SubclassOf(left), Type::SubclassOf(right)) => {
|
||||
match (left.subclass_of(), right.subclass_of()) {
|
||||
(ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(&right),
|
||||
(ClassBase::Class(_), _) => Ordering::Less,
|
||||
(_, ClassBase::Class(_)) => Ordering::Greater,
|
||||
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
|
||||
dynamic_elements_ordering(left, right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Type::SubclassOf(_), _) => Ordering::Less,
|
||||
(_, Type::SubclassOf(_)) => Ordering::Greater,
|
||||
(
|
||||
Type::Instance(InstanceType { class: left }),
|
||||
Type::Instance(InstanceType { class: right }),
|
||||
) => left.cmp(right),
|
||||
|
||||
(Type::Instance(_), _) => Ordering::Less,
|
||||
(_, Type::Instance(_)) => Ordering::Greater,
|
||||
|
||||
(Type::AlwaysTruthy, _) => Ordering::Less,
|
||||
(_, Type::AlwaysTruthy) => Ordering::Greater,
|
||||
|
||||
(Type::AlwaysFalsy, _) => Ordering::Less,
|
||||
(_, Type::AlwaysFalsy) => Ordering::Greater,
|
||||
|
||||
(Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) => {
|
||||
match (left_instance, right_instance) {
|
||||
(KnownInstanceType::Any, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Any) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Tuple, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Tuple) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::AlwaysFalsy, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::AlwaysFalsy) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::AlwaysTruthy, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::AlwaysTruthy) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Annotated, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Annotated) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Callable, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Callable) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::ChainMap, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::ChainMap) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::ClassVar, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::ClassVar) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Concatenate, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Concatenate) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Counter, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Counter) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::DefaultDict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::DefaultDict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Deque, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Deque) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Dict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Dict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Final, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Final) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::FrozenSet, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::FrozenSet) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeGuard, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeGuard) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::List, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::List) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Literal, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Literal) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::LiteralString, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::LiteralString) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Optional, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Optional) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::OrderedDict, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::OrderedDict) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::NoReturn, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::NoReturn) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Never, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Never) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Set, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Set) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Type, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Type) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeAlias, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeAlias) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Unknown, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Unknown) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Not, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Not) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Intersection, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Intersection) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeOf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeOf) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Unpack, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Unpack) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypingSelf, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypingSelf) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Required, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Required) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::NotRequired, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::NotRequired) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeIs, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeIs) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::ReadOnly, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::ReadOnly) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::Union, _) => Ordering::Less,
|
||||
(_, KnownInstanceType::Union) => Ordering::Greater,
|
||||
|
||||
(
|
||||
KnownInstanceType::TypeAliasType(left),
|
||||
KnownInstanceType::TypeAliasType(right),
|
||||
) => left.cmp(right),
|
||||
(KnownInstanceType::TypeAliasType(_), _) => Ordering::Less,
|
||||
(_, KnownInstanceType::TypeAliasType(_)) => Ordering::Greater,
|
||||
|
||||
(KnownInstanceType::TypeVar(left), KnownInstanceType::TypeVar(right)) => {
|
||||
left.cmp(right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(Type::KnownInstance(_), _) => Ordering::Less,
|
||||
(_, Type::KnownInstance(_)) => Ordering::Greater,
|
||||
|
||||
(Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right),
|
||||
(Type::Dynamic(_), _) => Ordering::Less,
|
||||
(_, Type::Dynamic(_)) => Ordering::Greater,
|
||||
|
||||
(Type::Union(left), Type::Union(right)) => left.cmp(right),
|
||||
(Type::Union(_), _) => Ordering::Less,
|
||||
(_, Type::Union(_)) => Ordering::Greater,
|
||||
|
||||
(Type::Intersection(left), Type::Intersection(right)) => left.cmp(right),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine a canonical order for two instances of [`DynamicType`].
|
||||
fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering {
|
||||
match (left, right) {
|
||||
(DynamicType::Any, _) => Ordering::Less,
|
||||
(_, DynamicType::Any) => Ordering::Greater,
|
||||
|
||||
(DynamicType::Unknown, _) => Ordering::Less,
|
||||
(_, DynamicType::Unknown) => Ordering::Greater,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
(DynamicType::Todo(left), DynamicType::Todo(right)) => match (left, right) {
|
||||
(
|
||||
TodoType::FileAndLine(left_file, left_line),
|
||||
TodoType::FileAndLine(right_file, right_line),
|
||||
) => left_file
|
||||
.cmp(right_file)
|
||||
.then_with(|| left_line.cmp(&right_line)),
|
||||
(TodoType::FileAndLine(..), _) => Ordering::Less,
|
||||
(_, TodoType::FileAndLine(..)) => Ordering::Greater,
|
||||
|
||||
(TodoType::Message(left), TodoType::Message(right)) => left.cmp(right),
|
||||
},
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
red_knot_project = { workspace = true }
|
||||
red_knot_workspace = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
@@ -40,7 +40,7 @@ pub(super) fn try_show_message(
|
||||
/// Sends an error to the client with a formatted message. The error is sent in a
|
||||
/// `window/showMessage` notification.
|
||||
macro_rules! show_err_msg {
|
||||
($msg:expr$(, $($arg:tt)*)?) => {
|
||||
crate::message::show_message(::core::format_args!($msg$(, $($arg)*)?).to_string(), lsp_types::MessageType::ERROR)
|
||||
($msg:expr$(, $($arg:tt),*)?) => {
|
||||
crate::message::show_message(::core::format_args!($msg, $($($arg),*)?).to_string(), lsp_types::MessageType::ERROR)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,11 +86,13 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
|
||||
return Box::new(|_, _| {});
|
||||
};
|
||||
let db = match path {
|
||||
AnySystemPath::System(path) => match session.project_db_for_path(path.as_std_path()) {
|
||||
Some(db) => db.clone(),
|
||||
None => session.default_project_db().clone(),
|
||||
},
|
||||
AnySystemPath::SystemVirtual(_) => session.default_project_db().clone(),
|
||||
AnySystemPath::System(path) => {
|
||||
match session.workspace_db_for_path(path.as_std_path()) {
|
||||
Some(db) => db.clone(),
|
||||
None => session.default_workspace_db().clone(),
|
||||
}
|
||||
}
|
||||
AnySystemPath::SystemVirtual(_) => session.default_workspace_db().clone(),
|
||||
};
|
||||
|
||||
let Some(snapshot) = session.take_snapshot(url) else {
|
||||
|
||||
@@ -2,7 +2,7 @@ use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::DidChangeTextDocument;
|
||||
use lsp_types::DidChangeTextDocumentParams;
|
||||
|
||||
use red_knot_project::watch::ChangeEvent;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::api::LSPResult;
|
||||
@@ -36,14 +36,14 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
||||
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.project_db_for_path_mut(path.as_std_path()) {
|
||||
let db = match session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
None => session.default_workspace_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::file_content_changed(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.apply_changes(vec![ChangeEvent::ChangedVirtual(virtual_path)], None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::DidCloseTextDocument;
|
||||
use lsp_types::DidCloseTextDocumentParams;
|
||||
use red_knot_project::watch::ChangeEvent;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
|
||||
use crate::server::api::diagnostics::clear_diagnostics;
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
@@ -34,7 +34,7 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
if let AnySystemPath::SystemVirtual(virtual_path) = path {
|
||||
let db = session.default_project_db_mut();
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use lsp_types::notification::DidCloseNotebookDocument;
|
||||
use lsp_types::DidCloseNotebookDocumentParams;
|
||||
|
||||
use red_knot_project::watch::ChangeEvent;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
use crate::server::api::LSPResult;
|
||||
@@ -33,7 +33,7 @@ impl SyncNotificationHandler for DidCloseNotebookHandler {
|
||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
if let AnySystemPath::SystemVirtual(virtual_path) = path {
|
||||
let db = session.default_project_db_mut();
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use lsp_types::notification::DidOpenTextDocument;
|
||||
use lsp_types::DidOpenTextDocumentParams;
|
||||
|
||||
use red_knot_project::watch::ChangeEvent;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||
@@ -33,14 +33,14 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
|
||||
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.project_db_for_path_mut(path.as_std_path()) {
|
||||
let db = match session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
None => session.default_workspace_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.files().virtual_file(db, &virtual_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use lsp_server::ErrorCode;
|
||||
use lsp_types::notification::DidOpenNotebookDocument;
|
||||
use lsp_types::DidOpenNotebookDocumentParams;
|
||||
|
||||
use red_knot_project::watch::ChangeEvent;
|
||||
use red_knot_workspace::watch::ChangeEvent;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::edit::NotebookDocument;
|
||||
@@ -41,14 +41,14 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
||||
|
||||
match path {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.project_db_for_path_mut(path.as_std_path()) {
|
||||
let db = match session.workspace_db_for_path_mut(path.as_std_path()) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
None => session.default_workspace_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(path)], None);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
let db = session.default_workspace_db_mut();
|
||||
db.files().virtual_file(db, &virtual_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::edit::ToRangeExt;
|
||||
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
|
||||
use crate::server::{client::Notifier, Result};
|
||||
use crate::session::DocumentSnapshot;
|
||||
use red_knot_project::{Db, ProjectDatabase};
|
||||
use red_knot_workspace::db::{Db, RootDatabase};
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
db: ProjectDatabase,
|
||||
db: RootDatabase,
|
||||
_notifier: Notifier,
|
||||
_params: DocumentDiagnosticParams,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
@@ -46,7 +46,7 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &ProjectDatabase) -> Vec<Diagnostic> {
|
||||
fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &RootDatabase) -> Vec<Diagnostic> {
|
||||
let Some(file) = snapshot.file(db) else {
|
||||
tracing::info!(
|
||||
"No file found for snapshot for `{}`",
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::session::{DocumentSnapshot, Session};
|
||||
|
||||
use lsp_types::notification::Notification as LSPNotification;
|
||||
use lsp_types::request::Request;
|
||||
use red_knot_project::ProjectDatabase;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
|
||||
/// A supertrait for any server request handler.
|
||||
pub(super) trait RequestHandler {
|
||||
@@ -34,7 +34,7 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
|
||||
|
||||
fn run_with_snapshot(
|
||||
snapshot: DocumentSnapshot,
|
||||
db: ProjectDatabase,
|
||||
db: RootDatabase,
|
||||
notifier: Notifier,
|
||||
params: <<Self as RequestHandler>::RequestType as Request>::Params,
|
||||
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
|
||||
|
||||
@@ -8,7 +8,8 @@ use std::sync::Arc;
|
||||
use anyhow::anyhow;
|
||||
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url};
|
||||
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::SystemPath;
|
||||
use ruff_db::Db;
|
||||
@@ -27,7 +28,7 @@ pub(crate) mod index;
|
||||
mod settings;
|
||||
|
||||
// TODO(dhruvmanila): In general, the server shouldn't use any salsa queries directly and instead
|
||||
// should use methods on `ProjectDatabase`.
|
||||
// should use methods on `RootDatabase`.
|
||||
|
||||
/// The global state for the LSP
|
||||
pub struct Session {
|
||||
@@ -40,9 +41,8 @@ pub struct Session {
|
||||
/// [`index_mut`]: Session::index_mut
|
||||
index: Option<Arc<index::Index>>,
|
||||
|
||||
/// Maps workspace folders to their respective project databases.
|
||||
projects_by_workspace_folder: BTreeMap<PathBuf, ProjectDatabase>,
|
||||
|
||||
/// Maps workspace root paths to their respective databases.
|
||||
workspaces: BTreeMap<PathBuf, RootDatabase>,
|
||||
/// The global position encoding, negotiated during LSP initialization.
|
||||
position_encoding: PositionEncoding,
|
||||
/// Tracks what LSP features the client supports and doesn't support.
|
||||
@@ -68,14 +68,14 @@ impl Session {
|
||||
let system = LSPSystem::new(index.clone());
|
||||
|
||||
// TODO(dhruvmanila): Get the values from the client settings
|
||||
let metadata = ProjectMetadata::discover(system_path, &system)?;
|
||||
let metadata = WorkspaceMetadata::discover(system_path, &system, None)?;
|
||||
// TODO(micha): Handle the case where the program settings are incorrect more gracefully.
|
||||
workspaces.insert(path, ProjectDatabase::new(metadata, system)?);
|
||||
workspaces.insert(path, RootDatabase::new(metadata, system)?);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
position_encoding,
|
||||
projects_by_workspace_folder: workspaces,
|
||||
workspaces,
|
||||
index: Some(index),
|
||||
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||
client_capabilities,
|
||||
@@ -87,41 +87,38 @@ impl Session {
|
||||
// and `default_workspace_db_mut` but the borrow checker doesn't allow that.
|
||||
// https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437
|
||||
|
||||
/// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if
|
||||
/// Returns a reference to the workspace [`RootDatabase`] corresponding to the given path, if
|
||||
/// any.
|
||||
pub(crate) fn project_db_for_path(&self, path: impl AsRef<Path>) -> Option<&ProjectDatabase> {
|
||||
self.projects_by_workspace_folder
|
||||
pub(crate) fn workspace_db_for_path(&self, path: impl AsRef<Path>) -> Option<&RootDatabase> {
|
||||
self.workspaces
|
||||
.range(..=path.as_ref().to_path_buf())
|
||||
.next_back()
|
||||
.map(|(_, db)| db)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the project [`ProjectDatabase`] corresponding to the given
|
||||
/// Returns a mutable reference to the workspace [`RootDatabase`] corresponding to the given
|
||||
/// path, if any.
|
||||
pub(crate) fn project_db_for_path_mut(
|
||||
pub(crate) fn workspace_db_for_path_mut(
|
||||
&mut self,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Option<&mut ProjectDatabase> {
|
||||
self.projects_by_workspace_folder
|
||||
) -> Option<&mut RootDatabase> {
|
||||
self.workspaces
|
||||
.range_mut(..=path.as_ref().to_path_buf())
|
||||
.next_back()
|
||||
.map(|(_, db)| db)
|
||||
}
|
||||
|
||||
/// Returns a reference to the default project [`ProjectDatabase`]. The default project is the
|
||||
/// minimum root path in the project map.
|
||||
pub(crate) fn default_project_db(&self) -> &ProjectDatabase {
|
||||
// SAFETY: Currently, red knot only support a single project.
|
||||
self.projects_by_workspace_folder.values().next().unwrap()
|
||||
/// Returns a reference to the default workspace [`RootDatabase`]. The default workspace is the
|
||||
/// minimum root path in the workspace map.
|
||||
pub(crate) fn default_workspace_db(&self) -> &RootDatabase {
|
||||
// SAFETY: Currently, red knot only support a single workspace.
|
||||
self.workspaces.values().next().unwrap()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the default project [`ProjectDatabase`].
|
||||
pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase {
|
||||
// SAFETY: Currently, red knot only support a single project.
|
||||
self.projects_by_workspace_folder
|
||||
.values_mut()
|
||||
.next()
|
||||
.unwrap()
|
||||
/// Returns a mutable reference to the default workspace [`RootDatabase`].
|
||||
pub(crate) fn default_workspace_db_mut(&mut self) -> &mut RootDatabase {
|
||||
// SAFETY: Currently, red knot only support a single workspace.
|
||||
self.workspaces.values_mut().next().unwrap()
|
||||
}
|
||||
|
||||
pub fn key_from_url(&self, url: Url) -> DocumentKey {
|
||||
@@ -190,7 +187,7 @@ impl Session {
|
||||
fn index_mut(&mut self) -> MutIndexGuard {
|
||||
let index = self.index.take().unwrap();
|
||||
|
||||
for db in self.projects_by_workspace_folder.values_mut() {
|
||||
for db in self.workspaces.values_mut() {
|
||||
// Remove the `index` from each database. This drops the count of `Arc<Index>` down to 1
|
||||
db.system_mut()
|
||||
.as_any_mut()
|
||||
@@ -235,7 +232,7 @@ impl Drop for MutIndexGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(index) = self.index.take() {
|
||||
let index = Arc::new(index);
|
||||
for db in self.session.projects_by_workspace_folder.values_mut() {
|
||||
for db in self.session.workspaces.values_mut() {
|
||||
db.system_mut()
|
||||
.as_any_mut()
|
||||
.downcast_mut::<LSPSystem>()
|
||||
@@ -270,7 +267,7 @@ impl DocumentSnapshot {
|
||||
self.position_encoding
|
||||
}
|
||||
|
||||
pub(crate) fn file(&self, db: &ProjectDatabase) -> Option<File> {
|
||||
pub(crate) fn file(&self, db: &RootDatabase) -> Option<File> {
|
||||
match url_to_any_system_path(self.document_ref.file_url()).ok()? {
|
||||
AnySystemPath::System(path) => system_path_to_file(db, path).ok(),
|
||||
AnySystemPath::SystemVirtual(virtual_path) => db
|
||||
|
||||
@@ -257,21 +257,6 @@ x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
````
|
||||
|
||||
## Running the tests
|
||||
|
||||
All Markdown-based tests are executed in a normal `cargo test` / `cargo run nextest` run. If you want to run the Markdown tests
|
||||
*only*, you can filter the tests using `mdtest__`:
|
||||
|
||||
```bash
|
||||
cargo test -p red_knot_python_semantic -- mdtest__
|
||||
```
|
||||
|
||||
Alternatively, you can use the `mdtest.py` runner which has a watch mode that will re-run corresponding tests when Markdown files change, and recompile automatically when Rust code changes:
|
||||
|
||||
```bash
|
||||
uv -q run crates/red_knot_python_semantic/mdtest.py
|
||||
```
|
||||
|
||||
## Planned features
|
||||
|
||||
There are some designed features that we intend for the test framework to have, but have not yet
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_db::{Db as SourceDb, Upcast};
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Db {
|
||||
project_root: SystemPathBuf,
|
||||
workspace_root: SystemPathBuf,
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
@@ -20,11 +20,11 @@ pub(crate) struct Db {
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(project_root: SystemPathBuf) -> Self {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let rule_selection = RuleSelection::from_registry(default_lint_registry());
|
||||
|
||||
let db = Self {
|
||||
project_root,
|
||||
workspace_root,
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
@@ -33,15 +33,15 @@ impl Db {
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&db.project_root)
|
||||
.create_directory_all(&db.workspace_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings::new(vec![db.project_root.clone()]),
|
||||
search_paths: SearchPathSettings::new(db.workspace_root.clone()),
|
||||
},
|
||||
)
|
||||
.expect("Invalid search path settings");
|
||||
@@ -49,8 +49,8 @@ impl Db {
|
||||
db
|
||||
}
|
||||
|
||||
pub(crate) fn project_root(&self) -> &SystemPath {
|
||||
&self.project_root
|
||||
pub(crate) fn workspace_root(&self) -> &SystemPath {
|
||||
&self.workspace_root
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
}
|
||||
|
||||
fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures> {
|
||||
let project_root = db.project_root().to_path_buf();
|
||||
let workspace_root = db.workspace_root().to_path_buf();
|
||||
|
||||
let test_files: Vec<_> = test
|
||||
.files()
|
||||
@@ -110,7 +110,7 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
matches!(embedded.lang, "py" | "pyi"),
|
||||
"Non-Python files not supported yet."
|
||||
);
|
||||
let full_path = project_root.join(embedded.path);
|
||||
let full_path = workspace_root.join(embedded.path);
|
||||
db.write_file(&full_path, embedded.code).unwrap();
|
||||
let file = system_path_to_file(db, full_path).unwrap();
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from typing import Any, LiteralString, _SpecialForm
|
||||
from typing import _SpecialForm, Any, LiteralString
|
||||
|
||||
# Special operations
|
||||
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
|
||||
|
||||
# Types
|
||||
Unknown = object()
|
||||
AlwaysTruthy = object()
|
||||
AlwaysFalsy = object()
|
||||
|
||||
# Special forms
|
||||
Not: _SpecialForm
|
||||
@@ -18,10 +16,9 @@ TypeOf: _SpecialForm
|
||||
# Ideally, these would be annotated using `TypeForm`, but that has not been
|
||||
# standardized yet (https://peps.python.org/pep-0747).
|
||||
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
|
||||
def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ...
|
||||
def is_subtype_of(type_derived: Any, typ_ebase: Any) -> bool: ...
|
||||
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
|
||||
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
|
||||
def is_gradual_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
|
||||
def is_fully_static(type: Any) -> bool: ...
|
||||
def is_singleton(type: Any) -> bool: ...
|
||||
def is_single_valued(type: Any) -> bool: ...
|
||||
|
||||
@@ -21,7 +21,7 @@ the project the stubs are for, but instead report them here to typeshed.**
|
||||
Further documentation on stub files, typeshed, and Python's typing system in
|
||||
general, can also be found at https://typing.readthedocs.io/en/latest/.
|
||||
|
||||
Typeshed supports Python versions 3.9 to 3.13.
|
||||
Typeshed supports Python versions 3.8 to 3.13.
|
||||
|
||||
## Using
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
101287091cbd71a3305a4fc4a1a8eb5df0e3f6f7
|
||||
2047b820730fdd65d37e6e8efcf29ca9af7ec3e7
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user