Merge main and reconcile type relation imports

This commit is contained in:
Carl Meyer committed 2026-10-27 12:29:23 -07:00
commit 8f94a11b76
117 files changed
+1099 -427

No files matched your search

+1 -1
View File
@@ -26,6 +26,6 @@ body:
attributes:
label: Version
description: What version of ruff are you using? (see `ruff version`)
placeholder: e.g., ruff 0.17.2
placeholder: e.g., ruff 0.17.3
validations:
required: false
+1 -1
View File
@@ -13,6 +13,6 @@ body:
attributes:
label: Version
description: What version of ruff are you using? (see `ruff version`)
placeholder: e.g., ruff 0.17.2
placeholder: e.g., ruff 0.17.3
validations:
required: false
+28
View File
@@ -1,5 +1,33 @@
# Changelog
## 0.17.3
Released on 2026-10-26.
### Preview features
- \[`suspicious`\] Add `shared-clock-mock` ([#29074](https://github.com/astral-sh/ruff/pull/29074))
### Bug fixes
- \[`pyupgrade`\] Avoid incorrect conversions to f-strings (`UP032`) ([#25485](https://github.com/astral-sh/ruff/pull/25485))
### Performance
- Buffer stdout in `ruff format` ([#29631](https://github.com/astral-sh/ruff/pull/29631))
- Specialize ASCII identifier lexing ([#29503](https://github.com/astral-sh/ruff/pull/29503))
- Speed up identifier and keyword lexing ([#29463](https://github.com/astral-sh/ruff/pull/29463))
### Documentation
- \[`flake8-bandit`\] Describe what the rule actually checks for `httpx` (`S113`) ([#27936](https://github.com/astral-sh/ruff/pull/27936))
### Contributors
- [@itsvedantkumar](https://github.com/itsvedantkumar)
- [@charliermarsh](https://github.com/charliermarsh)
- [@anishgirianish](https://github.com/anishgirianish)
## 0.17.2
Released on 2026-10-22.
Generated
+41 -41
View File
@@ -3098,7 +3098,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.17.2"
version = "0.17.3"
dependencies = [
"anyhow",
"assert_fs",
@@ -3162,7 +3162,7 @@ dependencies = [
[[package]]
name = "ruff_annotate_snippets"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anstream 1.0.0",
"anstyle",
@@ -3206,7 +3206,7 @@ dependencies = [
[[package]]
name = "ruff_cache"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"char_str",
"filetime",
@@ -3220,14 +3220,14 @@ dependencies = [
[[package]]
name = "ruff_command_line"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"argfile",
]
[[package]]
name = "ruff_db"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anstyle",
"arc-swap",
@@ -3319,7 +3319,7 @@ dependencies = [
[[package]]
name = "ruff_diagnostics"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"get-size2",
"is-macro",
@@ -3329,7 +3329,7 @@ dependencies = [
[[package]]
name = "ruff_formatter"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"drop_bomb",
"ruff_cache",
@@ -3345,7 +3345,7 @@ dependencies = [
[[package]]
name = "ruff_graph"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"clap",
@@ -3366,7 +3366,7 @@ dependencies = [
[[package]]
name = "ruff_index"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"get-size2",
"ruff_macros",
@@ -3376,7 +3376,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.17.2"
version = "0.17.3"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3438,7 +3438,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"heck",
"itertools 0.15.0",
@@ -3451,7 +3451,7 @@ dependencies = [
[[package]]
name = "ruff_markdown"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"insta",
"regex",
@@ -3482,14 +3482,14 @@ dependencies = [
[[package]]
name = "ruff_memory_usage"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"get-size2",
]
[[package]]
name = "ruff_notebook"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"rand 0.10.3",
@@ -3505,14 +3505,14 @@ dependencies = [
[[package]]
name = "ruff_options_metadata"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"serde",
]
[[package]]
name = "ruff_python_ast"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"aho-corasick",
"arrayvec",
@@ -3549,7 +3549,7 @@ dependencies = [
[[package]]
name = "ruff_python_codegen"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"ruff_python_ast",
"ruff_python_literal",
@@ -3561,7 +3561,7 @@ dependencies = [
[[package]]
name = "ruff_python_edits"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"ruff_python_ast",
"ruff_python_parser",
@@ -3571,7 +3571,7 @@ dependencies = [
[[package]]
name = "ruff_python_formatter"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"clap",
@@ -3604,7 +3604,7 @@ dependencies = [
[[package]]
name = "ruff_python_importer"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"insta",
@@ -3619,7 +3619,7 @@ dependencies = [
[[package]]
name = "ruff_python_index"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"ruff_python_ast",
"ruff_python_parser",
@@ -3630,7 +3630,7 @@ dependencies = [
[[package]]
name = "ruff_python_literal"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"bitflags 2.13.2",
"icu_normalizer",
@@ -3641,7 +3641,7 @@ dependencies = [
[[package]]
name = "ruff_python_parser"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"bitflags 2.13.2",
@@ -3672,7 +3672,7 @@ dependencies = [
[[package]]
name = "ruff_python_semantic"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"bitflags 2.13.2",
"insta",
@@ -3693,7 +3693,7 @@ dependencies = [
[[package]]
name = "ruff_python_stdlib"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"bitflags 2.13.2",
"unicode-ident",
@@ -3701,7 +3701,7 @@ dependencies = [
[[package]]
name = "ruff_python_trivia"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"itertools 0.15.0",
"memchr",
@@ -3724,7 +3724,7 @@ dependencies = [
[[package]]
name = "ruff_ranged_value"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"get-size2",
"ruff_db",
@@ -3737,7 +3737,7 @@ dependencies = [
[[package]]
name = "ruff_server"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"crossbeam",
@@ -3781,7 +3781,7 @@ dependencies = [
[[package]]
name = "ruff_source_file"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"get-size2",
"memchr",
@@ -3791,7 +3791,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"get-size2",
"schemars",
@@ -3802,7 +3802,7 @@ dependencies = [
[[package]]
name = "ruff_unicode_names2"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"phf 0.14.0",
"rand 0.10.3",
@@ -3811,7 +3811,7 @@ dependencies = [
[[package]]
name = "ruff_unicode_names2_generator"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"phf_codegen 0.14.0",
"rand 0.10.3",
@@ -3819,7 +3819,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.17.2"
version = "0.17.3"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3846,7 +3846,7 @@ dependencies = [
[[package]]
name = "ruff_workspace"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"colored",
@@ -4729,7 +4729,7 @@ dependencies = [
[[package]]
name = "ty_combine"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"ordermap",
"ruff_db",
@@ -4810,7 +4810,7 @@ dependencies = [
[[package]]
name = "ty_module_resolver"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"camino",
@@ -4891,7 +4891,7 @@ dependencies = [
[[package]]
name = "ty_python_core"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"bitflags 2.13.2",
@@ -4926,7 +4926,7 @@ dependencies = [
[[package]]
name = "ty_python_semantic"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"anyhow",
"bitflags 2.13.2",
@@ -5027,7 +5027,7 @@ dependencies = [
[[package]]
name = "ty_site_packages"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"camino",
"colored",
@@ -5048,7 +5048,7 @@ dependencies = [
[[package]]
name = "ty_static"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"ruff_macros",
]
@@ -5081,7 +5081,7 @@ dependencies = [
[[package]]
name = "ty_vendored"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"path-slash",
"ruff_db",
+40 -40
View File
@@ -14,55 +14,55 @@ license = "MIT"
[workspace.dependencies]
char_str = { version = "0.0.4", default-features = false, features = ["std"] }
ruff = { version = "0.17.2", path = "crates/ruff" }
ruff_annotate_snippets = { version = "0.0.19", path = "crates/ruff_annotate_snippets" }
ruff_cache = { version = "0.0.19", path = "crates/ruff_cache" }
ruff_command_line = { version = "0.0.19", path = "crates/ruff_command_line" }
ruff_db = { version = "0.0.19", path = "crates/ruff_db", default-features = false }
ruff_diagnostics = { version = "0.0.19", path = "crates/ruff_diagnostics" }
ruff_formatter = { version = "0.0.19", path = "crates/ruff_formatter" }
ruff_graph = { version = "0.0.19", path = "crates/ruff_graph" }
ruff_index = { version = "0.0.19", path = "crates/ruff_index" }
ruff_linter = { version = "0.17.2", path = "crates/ruff_linter" }
ruff_macros = { version = "0.0.19", path = "crates/ruff_macros" }
ruff_markdown = { version = "0.0.19", path = "crates/ruff_markdown" }
ruff_memory_usage = { version = "0.0.19", path = "crates/ruff_memory_usage" }
ruff_notebook = { version = "0.0.19", path = "crates/ruff_notebook" }
ruff_options_metadata = { version = "0.0.19", path = "crates/ruff_options_metadata" }
ruff_python_ast = { version = "0.0.19", path = "crates/ruff_python_ast" }
ruff_python_codegen = { version = "0.0.19", path = "crates/ruff_python_codegen" }
ruff_python_edits = { version = "0.0.19", path = "crates/ruff_python_edits" }
ruff_python_formatter = { version = "0.0.19", path = "crates/ruff_python_formatter" }
ruff_python_importer = { version = "0.0.19", path = "crates/ruff_python_importer" }
ruff_python_index = { version = "0.0.19", path = "crates/ruff_python_index" }
ruff_python_literal = { version = "0.0.19", path = "crates/ruff_python_literal" }
ruff_python_parser = { version = "0.0.19", path = "crates/ruff_python_parser" }
ruff_python_semantic = { version = "0.0.19", path = "crates/ruff_python_semantic" }
ruff_python_stdlib = { version = "0.0.19", path = "crates/ruff_python_stdlib" }
ruff_python_trivia = { version = "0.0.19", path = "crates/ruff_python_trivia" }
ruff_server = { version = "0.0.19", path = "crates/ruff_server" }
ruff_source_file = { version = "0.0.19", path = "crates/ruff_source_file" }
ruff = { version = "0.17.3", path = "crates/ruff" }
ruff_annotate_snippets = { version = "0.0.20", path = "crates/ruff_annotate_snippets" }
ruff_cache = { version = "0.0.20", path = "crates/ruff_cache" }
ruff_command_line = { version = "0.0.20", path = "crates/ruff_command_line" }
ruff_db = { version = "0.0.20", path = "crates/ruff_db", default-features = false }
ruff_diagnostics = { version = "0.0.20", path = "crates/ruff_diagnostics" }
ruff_formatter = { version = "0.0.20", path = "crates/ruff_formatter" }
ruff_graph = { version = "0.0.20", path = "crates/ruff_graph" }
ruff_index = { version = "0.0.20", path = "crates/ruff_index" }
ruff_linter = { version = "0.17.3", path = "crates/ruff_linter" }
ruff_macros = { version = "0.0.20", path = "crates/ruff_macros" }
ruff_markdown = { version = "0.0.20", path = "crates/ruff_markdown" }
ruff_memory_usage = { version = "0.0.20", path = "crates/ruff_memory_usage" }
ruff_notebook = { version = "0.0.20", path = "crates/ruff_notebook" }
ruff_options_metadata = { version = "0.0.20", path = "crates/ruff_options_metadata" }
ruff_python_ast = { version = "0.0.20", path = "crates/ruff_python_ast" }
ruff_python_codegen = { version = "0.0.20", path = "crates/ruff_python_codegen" }
ruff_python_edits = { version = "0.0.20", path = "crates/ruff_python_edits" }
ruff_python_formatter = { version = "0.0.20", path = "crates/ruff_python_formatter" }
ruff_python_importer = { version = "0.0.20", path = "crates/ruff_python_importer" }
ruff_python_index = { version = "0.0.20", path = "crates/ruff_python_index" }
ruff_python_literal = { version = "0.0.20", path = "crates/ruff_python_literal" }
ruff_python_parser = { version = "0.0.20", path = "crates/ruff_python_parser" }
ruff_python_semantic = { version = "0.0.20", path = "crates/ruff_python_semantic" }
ruff_python_stdlib = { version = "0.0.20", path = "crates/ruff_python_stdlib" }
ruff_python_trivia = { version = "0.0.20", path = "crates/ruff_python_trivia" }
ruff_server = { version = "0.0.20", path = "crates/ruff_server" }
ruff_source_file = { version = "0.0.20", path = "crates/ruff_source_file" }
ruff_mdtest = { path = "crates/ruff_mdtest" }
ruff_ranged_value = { version = "0.0.19", path = "crates/ruff_ranged_value" }
ruff_text_size = { version = "0.0.19", path = "crates/ruff_text_size" }
ruff_unicode_names2 = { version = "0.0.19", path = "crates/ruff_unicode_names2" }
ruff_unicode_names2_generator = { version = "0.0.19", path = "crates/ruff_unicode_names2_generator" }
ruff_workspace = { version = "0.0.19", path = "crates/ruff_workspace" }
ruff_ranged_value = { version = "0.0.20", path = "crates/ruff_ranged_value" }
ruff_text_size = { version = "0.0.20", path = "crates/ruff_text_size" }
ruff_unicode_names2 = { version = "0.0.20", path = "crates/ruff_unicode_names2" }
ruff_unicode_names2_generator = { version = "0.0.20", path = "crates/ruff_unicode_names2_generator" }
ruff_workspace = { version = "0.0.20", path = "crates/ruff_workspace" }
ty = { path = "crates/ty" }
ty_combine = { version = "0.0.19", path = "crates/ty_combine" }
ty_combine = { version = "0.0.20", path = "crates/ty_combine" }
ty_completion_bench = { path = "crates/ty_completion_bench" }
ty_completion_eval = { path = "crates/ty_completion_eval" }
ty_ide = { path = "crates/ty_ide" }
ty_module_resolver = { version = "0.0.19", path = "crates/ty_module_resolver" }
ty_module_resolver = { version = "0.0.20", path = "crates/ty_module_resolver" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { version = "0.0.19", path = "crates/ty_python_semantic" }
ty_python_core = { version = "0.0.19", path = "crates/ty_python_core" }
ty_python_semantic = { version = "0.0.20", path = "crates/ty_python_semantic" }
ty_python_core = { version = "0.0.20", path = "crates/ty_python_core" }
ty_server = { path = "crates/ty_server" }
ty_site_packages = { version = "0.0.19", path = "crates/ty_site_packages" }
ty_static = { version = "0.0.19", path = "crates/ty_static" }
ty_site_packages = { version = "0.0.20", path = "crates/ty_site_packages" }
ty_static = { version = "0.0.20", path = "crates/ty_static" }
ty_test = { path = "crates/ty_test" }
ty_vendored = { version = "0.0.19", path = "crates/ty_vendored" }
ty_vendored = { version = "0.0.20", path = "crates/ty_vendored" }
mdtest = { path = "crates/mdtest" }
+5 -5
View File
@@ -122,8 +122,8 @@ Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI.
Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/):
```shell
uvx ruff@0.17.2 check # Lint all files in the current directory.
uvx ruff@0.17.2 format # Format all files in the current directory.
uvx ruff@0.17.3 check # Lint all files in the current directory.
uvx ruff@0.17.3 format # Format all files in the current directory.
```
Or install Ruff with `uv` (recommended), `pip`, or `pipx`:
@@ -150,8 +150,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.17.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.17.2/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.17.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.17.3/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -184,7 +184,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.17.2
rev: v0.17.3
hooks:
# Run the linter.
- id: ruff-check
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.17.2"
version = "0.17.3"
description = "An extremely fast Python linter and code formatter"
authors = { workspace = true }
edition = { workspace = true }
+1 -1
View File
@@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or
This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not
considered public interface.
This is version 0.17.2. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff).
This is version 0.17.3. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff).
The following Ruff workspace members are also available:
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_annotate_snippets"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_cache"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_cache).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_cache).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_command_line"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_command_line).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_command_line).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_db"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_db).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_db).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_diagnostics"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_diagnostics).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_diagnostics).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_formatter"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_formatter).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_formatter).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_graph"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
edition.workspace = true
rust-version.workspace = true
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_graph).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_graph).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_index"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_index).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_index).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.17.2"
version = "0.17.3"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.17.2) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_linter).
This version (0.17.3) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_linter).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
@@ -30,6 +30,19 @@ use crate::package::PackageRoot;
/// `__init__.py` file is typically meant to be a regular package, and
/// the absence of the `__init__.py` file is probably an oversight.
///
/// ## Preview
///
/// In preview, the rule also checks for packages without `__init__.py` files between two regular
/// packages, such as `bar` in the project structure below.
///
/// ```text
/// foo/
/// ├── __init__.py
/// └── bar/
/// └── baz/
/// └── __init__.py
/// ```
///
/// ## Options
/// - `namespace-packages`
#[derive(ViolationMetadata)]
@@ -155,89 +155,59 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) {
return;
}
// Generate a map from key to (index, value).
let mut seen: FxHashMap<HashableExpr, (&Expr, FxHashSet<ComparableExpr>)> =
FxHashMap::with_capacity_and_hasher(dict.len(), FxBuildHasher);
let mut seen_keys = FxHashMap::with_capacity_and_hasher(dict.len(), FxBuildHasher);
let mut seen_values = FxHashMap::default();
// Detect duplicate keys.
for (i, ast::DictItem { key, value }) in dict.iter().enumerate() {
let Some(key) = key else {
continue;
};
let comparable_key = HashableExpr::from(key);
let comparable_value = ComparableExpr::from(value);
match seen.entry(comparable_key) {
let (first_index, first_key) = match seen_keys.entry(HashableExpr::from(key)) {
Entry::Vacant(entry) => {
entry.insert((key, FxHashSet::from_iter([comparable_value])));
entry.insert((i, key));
continue;
}
Entry::Occupied(mut entry) => {
let (seen_key, seen_values) = entry.get_mut();
match key {
key if is_literal_key(key)
&& checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) =>
{
let mut diagnostic = checker.report_diagnostic(
MultiValueRepeatedKeyLiteral {
name: SourceCodeSnippet::from_str(checker.locator().slice(key)),
existing: SourceCodeSnippet::from_str(
checker.locator().slice(*seen_key),
),
},
key.range(),
);
if !seen_values.insert(comparable_value) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion(
parenthesized_range(
dict.value(i - 1).into(),
dict.into(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i - 1).range())
.end(),
parenthesized_range(
dict.value(i).into(),
dict.into(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i).range())
.end(),
)));
}
}
Expr::Name(_)
if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) =>
{
let mut diagnostic = checker.report_diagnostic(
MultiValueRepeatedKeyVariable {
name: SourceCodeSnippet::from_str(checker.locator().slice(key)),
},
key.range(),
);
let comparable_value: ComparableExpr = dict.value(i).into();
if !seen_values.insert(comparable_value) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion(
parenthesized_range(
dict.value(i - 1).into(),
dict.into(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i - 1).range())
.end(),
parenthesized_range(
dict.value(i).into(),
dict.into(),
checker.tokens(),
)
.unwrap_or_else(|| dict.value(i).range())
.end(),
)));
}
}
_ => {}
}
Entry::Occupied(entry) => *entry.get(),
};
let mut diagnostic = match key {
key if is_literal_key(key)
&& checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) =>
{
checker.report_diagnostic(
MultiValueRepeatedKeyLiteral {
name: SourceCodeSnippet::from_str(checker.locator().slice(key)),
existing: SourceCodeSnippet::from_str(checker.locator().slice(first_key)),
},
key.range(),
)
}
Expr::Name(_) if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) => {
checker.report_diagnostic(
MultiValueRepeatedKeyVariable {
name: SourceCodeSnippet::from_str(checker.locator().slice(key)),
},
key.range(),
)
}
_ => continue,
};
// Comparing values is only necessary to decide whether a repeated key can be fixed.
let values = seen_values.entry(first_index).or_insert_with(|| {
FxHashSet::from_iter([ComparableExpr::from(dict.value(first_index))])
});
if !values.insert(ComparableExpr::from(value)) {
let previous = dict.value(i - 1);
diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion(
parenthesized_range(previous.into(), dict.into(), checker.tokens())
.unwrap_or_else(|| previous.range())
.end(),
parenthesized_range(value.into(), dict.into(), checker.tokens())
.unwrap_or_else(|| value.range())
.end(),
)));
}
}
}
@@ -54,7 +54,7 @@ use crate::codes::Category;
/// - [Python documentation: Where to patch](https://docs.python.org/3/library/unittest.mock.html#where-to-patch)
/// - [pytest documentation: Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "NEXT_RUFF_VERSION", category = Category::Suspicious)]
#[violation_metadata(preview_since = "0.17.3", category = Category::Suspicious)]
pub(crate) struct SharedClockMock {
function: String,
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_macros).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_macros).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_markdown"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
edition = { workspace = true }
rust-version = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_markdown).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_markdown).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_memory_usage"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_memory_usage).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_memory_usage).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_notebook"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_notebook).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_notebook).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_options_metadata"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_options_metadata).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_options_metadata).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_ast"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_ast).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_ast).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_codegen"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_codegen).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_codegen).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_edits"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_edits).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_edits).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_formatter"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_formatter).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_formatter).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_importer"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_importer).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_importer).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_index"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_index).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_index).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_literal"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>", "RustPython Team"]
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_literal).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_literal).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_parser"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>", "RustPython Team"]
edition = { workspace = true }
+2 -2
View File
@@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_parser).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_parser).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
@@ -0,0 +1,2 @@
try: work()
except ValueError: raise; report()
@@ -532,7 +532,10 @@ impl<'src> Parser<'src> {
self.bump(TokenKind::Raise);
let exc = match self.current_token_kind() {
TokenKind::Newline => None,
// test_ok bare_raise_before_semicolon
// try: work()
// except ValueError: raise; report()
TokenKind::Newline | TokenKind::Semi => None,
TokenKind::From => {
// test_err raise_stmt_from_without_exc
// raise from exc
@@ -0,0 +1,108 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py
---
## AST
```
Module(
ModModule {
node_index: NodeIndex(None),
range: 0..47,
body: [
Try(
StmtTry {
node_index: NodeIndex(None),
range: 0..46,
body: [
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 5..11,
value: Call(
ExprCall {
node_index: NodeIndex(None),
range: 5..11,
func: Name(
ExprName {
node_index: NodeIndex(None),
range: 5..9,
id: Name("work"),
ctx: Load,
},
),
arguments: Arguments {
range: 9..11,
node_index: NodeIndex(None),
args: [],
keywords: [],
},
},
),
},
),
],
handlers: [
ExceptHandler(
ExceptHandlerExceptHandler {
range: 12..46,
node_index: NodeIndex(None),
type_: Some(
Name(
ExprName {
node_index: NodeIndex(None),
range: 19..29,
id: Name("ValueError"),
ctx: Load,
},
),
),
name: None,
body: [
Raise(
StmtRaise {
node_index: NodeIndex(None),
range: 31..36,
exc: None,
cause: None,
},
),
Expr(
StmtExpr {
node_index: NodeIndex(None),
range: 38..46,
value: Call(
ExprCall {
node_index: NodeIndex(None),
range: 38..46,
func: Name(
ExprName {
node_index: NodeIndex(None),
range: 38..44,
id: Name("report"),
ctx: Load,
},
),
arguments: Arguments {
range: 44..46,
node_index: NodeIndex(None),
args: [],
keywords: [],
},
},
),
},
),
],
},
),
],
orelse: [],
finalbody: [],
is_star: false,
},
),
],
},
)
```
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_semantic"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_semantic).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_semantic).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_stdlib"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_stdlib).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_stdlib).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_python_trivia"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_trivia).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_trivia).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_ranged_value"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_ranged_value).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_ranged_value).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_server"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh).
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_server).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_server).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_source_file"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_source_file).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_source_file).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_text_size"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_text_size).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_text_size).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_unicode_names2"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
@@ -1,6 +1,6 @@
[package]
name = "ruff_unicode_names2_generator"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_unicode_names2_generator).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_unicode_names2_generator).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.17.2"
version = "0.17.3"
description = "WebAssembly bindings for Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument);
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.17.2) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_wasm).
This version (0.17.3) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_wasm).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ruff_workspace"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_workspace).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_workspace).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ty_combine"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
edition.workspace = true
rust-version.workspace = true
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_combine).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_combine).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ty_module_resolver"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_module_resolver).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_module_resolver).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ty_python_core"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_python_core).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_python_core).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
+11 -16
View File
@@ -3771,7 +3771,10 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
fn visit_bool_expression(&mut self, node: &'ast ast::ExprBoolOp, context: ExpressionContext) {
let ast::ExprBoolOp { values, op, .. } = node;
let mut snapshots = vec![];
let mut reachability_constraints = vec![];
let has_named_expression = values
.iter()
.any(|value| any_over_expr(value, &ast::Expr::is_named_expr));
let mut reachability_constraints = SmallVec::<[ScopedReachabilityConstraintId; 1]>::new();
let mut last_condition_flow_snapshots = None;
for (index, value) in values.iter().enumerate() {
@@ -3812,19 +3815,17 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
self.flow_snapshot()
};
// We first model the short-circuiting behavior. We take the short-circuit
// path here if all of the previous short-circuit paths were not taken, so
// we record all previously existing reachability constraints, and negate the
// one for the current expression.
// This path reaches the current operand and then short-circuits.
self.record_negated_reachability_constraint(reachability_constraint);
snapshots.push(self.flow_snapshot());
// Then we model the non-short-circuiting behavior. Here, we need to delay
// the application of the reachability constraint until after the expression
// has been evaluated, so we only push it onto the stack here.
self.flow_restore(continuation);
self.record_narrowing_constraint_id_for_places(predicate_id, &possibly_narrowed);
// The continuation carries the conditions that led to this operand. Named
// expressions can introduce new bindings that still need the earlier gates.
if !has_named_expression {
reachability_constraints.clear();
}
reachability_constraints.push(reachability_constraint);
} else {
last_condition_flow_snapshots = self.take_condition_flow_snapshots(value);
@@ -3838,13 +3839,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
condition_flow_snapshots.into_short_circuit_and_continuation(*op);
(Some(short_circuit), Some(no_short_circuit))
} else {
(
None,
values
.iter()
.any(|value| any_over_expr(value, &ast::Expr::is_named_expr))
.then(|| self.flow_snapshot()),
)
(None, has_named_expression.then(|| self.flow_snapshot()))
};
if let Some(last_short_circuit) = last_short_circuit {
+1 -1
View File
@@ -91,7 +91,7 @@ pub enum ExpressionContext {
pub enum ExpressionKind {
/// An ordinary value expression, such as `1` in `self.x: int = 1`.
Normal,
/// A predicate whose truthiness is recorded in its evaluation context.
/// A predicate evaluated in value or condition context.
Predicate {
context: ExpressionContext,
in_loop: bool,
+2 -1
View File
@@ -2193,7 +2193,8 @@ impl<'db> UseDefMapBuilder<'db> {
}
pub(super) fn add_place(&mut self, place: ScopedPlaceId) {
self.checkpoint_state.record_binding_change();
// Earlier snapshots restore new places as undefined. The first definition
// changes the bindings visible to an exception handler.
match place {
ScopedPlaceId::Symbol(symbol) => {
let new_place = self.symbol_states.push(PendingPlaceState::new(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "ty_python_semantic"
version = "0.0.19"
version = "0.0.20"
description = "This is an internal component crate of Ruff"
authors = { workspace = true }
edition = { workspace = true }
+2 -2
View File
@@ -5,8 +5,8 @@
This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed
here is unstable and will have frequent breaking changes.
This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_python_semantic).
This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The
source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_python_semantic).
See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for
details on versioning.
@@ -0,0 +1,11 @@
# Regression test for https://github.com/astral-sh/ty/issues/4834
class C("", (lambda: value, (predicate := lambda: value))):
pass
# Read the lambda before the loop adds the class as another possible value of predicate.
predicate
while True:
assert predicate
value = 0
predicate = C
@@ -319,6 +319,15 @@ def nested_impossible_operands(other: object, value: bool, marker: int):
reveal_type(marker) # revealed: Never
```
An assignment expression can rebind a name tested earlier in the chain. Reaching the final operand
requires the new value to be `None`, so the branch is unreachable.
```py
def reassigned_operand(value: int | None, replacement: int | None, marker: int):
if value is not None and (value := replacement) is None and value is None and isinstance(value, int):
reveal_type(marker) # revealed: Never
```
## Conditions with aliased `Never` operands
A call cannot produce a result when its return type is an alias of `Never`. Only the preceding
@@ -389,4 +389,156 @@ def _(article: NewsArticle):
reveal_type(Article.get_previous_by_published(article)) # revealed: NewsArticle
```
## Abstract models
### Basic
An abstract model provides reusable fields without creating its own database table. Its concrete
subclasses receive an integer primary key unless they declare or inherit an explicit primary key.
```py
from django.db import models
class AbstractBook(models.Model):
title = models.CharField(max_length=200)
class Meta:
abstract = True
class Ebook(AbstractBook):
file_size = models.IntegerField()
class Book(AbstractBook):
isbn = models.CharField(max_length=13, primary_key=True)
def _(abstract: AbstractBook, ebook: Ebook, book: Book):
# Abstract models do not have an automatic primary key.
abstract.id # error: [unresolved-attribute]
# Concrete models without an explicit primary key have an automatic integer key.
reveal_type(ebook.id) # revealed: int
reveal_type(ebook.pk) # revealed: int
reveal_type(ebook.title) # revealed: str
reveal_type(ebook.file_size) # revealed: int
# The explicit ISBN primary key prevents an automatic `id`.
book.id # error: [unresolved-attribute]
reveal_type(book.pk) # revealed: str
reveal_type(book.title) # revealed: str
reveal_type(book.isbn) # revealed: str
```
### Annotated abstract setting
A bare annotation for `Meta.abstract` does not make a model abstract.
```py
from django.db import models
class Book(models.Model):
class Meta:
abstract: bool
def _(book: Book):
reveal_type(book.id) # revealed: int
reveal_type(book.pk) # revealed: int
```
When an assignment follows the annotation, its value determines whether the model is abstract.
```py
class AbstractBook(models.Model):
class Meta:
abstract: bool
abstract = True
def _(abstract: AbstractBook):
abstract.id # error: [unresolved-attribute]
```
### Inherited Meta configuration
Django resets an abstract model's `Meta.abstract` to `False`. A child that subclasses this `Meta` is
concrete unless it explicitly sets `abstract = True` again.
```py
from django.db import models
class AbstractBook(models.Model):
class Meta:
abstract = True
class NotAbstract(AbstractBook):
# error: [invalid-attribute-override] "Meta"
class Meta(AbstractBook.Meta):
ordering = ["id"]
class Abstract(AbstractBook):
# error: [invalid-attribute-override] "Meta"
class Meta(AbstractBook.Meta):
abstract = True
def _(not_abstract: NotAbstract, abstract: Abstract):
# concrete models have an automatic primary key
not_abstract.id
# abstract models do not:
abstract.id # error: [unresolved-attribute]
```
Configuration inherited from an ordinary class retains its `abstract` setting.
```py
class Options:
abstract = True
class InheritedAbstract(models.Model):
class Meta(Options):
pass
def _(abstract: InheritedAbstract):
abstract.id # error: [unresolved-attribute]
```
### Multiple inheritance
An abstract base without a primary key does not shadow an inherited `id`, regardless of the base
class order. Regression test for <https://github.com/astral-sh/ty/issues/4822>.
```py
from uuid import uuid4
from django.db import models
class AbstractHasTitle(models.Model):
title = models.CharField(max_length=200)
class Meta:
abstract = True
class AbstractHasId(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4)
class Meta:
abstract = True
# Django allows separate `Meta` configurations, but ty currently reports a conflict between them.
# error: [invalid-attribute-override] "Incompatible inherited attribute `Meta`"
class Book1(AbstractHasTitle, AbstractHasId):
pass
# (see above)
# error: [invalid-attribute-override] "Incompatible inherited attribute `Meta`"
class Book2(AbstractHasId, AbstractHasTitle):
pass
def _(book1: Book1, book2: Book2):
reveal_type(book1.id) # revealed: UUID
reveal_type(book1.pk) # revealed: UUID
reveal_type(book1.title) # revealed: str
reveal_type(book2.id) # revealed: UUID
reveal_type(book2.pk) # revealed: UUID
reveal_type(book2.title) # revealed: str
```
[django-types]: https://github.com/sbdchd/django-types
@@ -1943,6 +1943,21 @@ for _ in range(1_000_000):
x = 2
```
The condition can depend on an earlier iteration. Here `y` starts at `2` and only ever receives `1`
or `2` from `x`, so the loop never decrements `x`.
```py
def f(n: int):
x = 1
y = 2
for _ in range(n):
if not y:
x, y = x - 1, x
else:
x, y = 2, x
reveal_type(x) # revealed: Literal[1, 2]
```
### Large reachability constraint graphs fall back to `Unknown`
```py
@@ -864,6 +864,29 @@ def loop_back():
value = Integer.Y
```
The comparison narrows a `str | int` to the matching builtin type. A plain string can equal
`String.X` and reach the `.name` access.
```py
def compare_with_enum_member(value: str | int):
if value == String.X:
reveal_type(value) # revealed: str
value.name # error: [unresolved-attribute]
if Integer.X == value:
reveal_type(value) # revealed: int
```
Members of other `IntEnum` classes can also compare equal to `Integer.X`.
```py
def compare_int_enums(value: IntEnum):
if value != Integer.X:
reveal_type(value) # revealed: IntEnum & ~Literal[Integer.X]
else:
reveal_type(value) # revealed: IntEnum
```
A custom `__new__` can replace the value declared in an `IntEnum` class body. We can still narrow
the members of `Foo`, whose runtime values are known, but must preserve all of `Shifted` because its
members' runtime values cannot be determined statically:
@@ -1501,6 +1524,27 @@ def correlated_materialized_pattern(left: Top[MaterializedT], right: Materialize
return 1
```
## `LiteralString` and string literals
A comparison can narrow a `LiteralString` operand to a particular value or exclude that value. The
concrete string keeps its type in either branch.
```py
from typing_extensions import LiteralString
def compare(other: LiteralString):
value = "a"
if value != other:
reveal_type(value) # revealed: Literal["a"]
reveal_type(other) # revealed: LiteralString & ~Literal["a"]
value + 1 # error: [unsupported-operator]
if other == value:
reveal_type(other) # revealed: Literal["a"]
else:
reveal_type(value) # revealed: Literal["a"]
```
## `LiteralString` and string-valued enums
`LiteralString` can be narrowed by comparison with a string-valued enum member that inherits `str`'s
@@ -1517,6 +1561,7 @@ from typing_extensions import LiteralString
class Color(StrEnum):
RED = "red"
BLUE = "blue"
def narrow_literal_string_with_enum(value: LiteralString | None):
if value == Color.RED:
@@ -1530,6 +1575,15 @@ def narrow_literal_string_with_enum(value: LiteralString | None):
reveal_type(value) # revealed: Literal["red"]
```
The enum member keeps its type when its value differs from the `LiteralString`.
```py
def compare_member(other: LiteralString):
color = Color.RED
if color != other:
reveal_type(color) # revealed: Literal[Color.RED]
```
## Module literals
Modules compare equal only to the same module object:
@@ -1442,6 +1442,19 @@ def _(colors: list[Color]):
reveal_type(inline) # revealed: list[Color]
```
Membership succeeds for the plain integer `1` as well as for the enum member `Code.OK`.
```py
from enum import IntEnum
class Code(IntEnum):
OK = 1
def check_membership(value: int):
if value in (Code.OK,):
reveal_type(value) # revealed: int
```
An enum that can have additional runtime members can still be narrowed by a membership test against
an explicit member. The other branch excludes that member without assuming that the declared members
are exhaustive.
@@ -2183,6 +2183,32 @@ def test_match_mapping_uses_get(value: CustomGet) -> None:
reveal_type(item) # revealed: object
```
Narrowing a mapping preserves the type of its entries when it still uses the standard `get` method.
A truthiness check can exclude empty dictionaries, and an `isinstance` check can add another class
to the subject's type.
```py
def match_nonempty_dict(value: dict[str, int]) -> None:
if value:
match value:
case {"item": item}:
reveal_type(item) # revealed: int
class Tagged: ...
def match_tagged_mapping(value: Mapping[str, bytes]) -> None:
if isinstance(value, Tagged):
match value:
case {"item": item}:
reveal_type(item) # revealed: bytes
def match_nonempty_custom_get(value: CustomGet) -> None:
if value:
match value:
case {"item": item}:
reveal_type(item) # revealed: object
```
A callable stored on the instance can also provide `get`. The captured value still has type
`object`.
@@ -4032,6 +4058,12 @@ class Color(StrEnum):
GREEN = "g"
BLUE = "b"
def match_string(value: str):
match value:
case Color.GREEN as captured:
reveal_type(value) # revealed: str
reveal_type(captured) # revealed: str
def test_literal_as_enum(x: Literal["g"]) -> None:
match x:
case Color.RED:
@@ -399,6 +399,31 @@ static_assert(is_assignable_to(type[Any], type[Arbitrary]))
static_assert(is_assignable_to(type[Any], type[object]))
```
### Narrowed instances with a gradual base
A class with a gradual base can match a union arm outside its known inheritance hierarchy, even
after narrowing rules out another type.
```py
from typing import Any
from missing import UnknownBase # error: [unresolved-import]
from ty_extensions import Intersection, Not, static_assert
from ty_extensions._internal import is_assignable_to
class A: ...
class B: ...
class FromAny(Any): ...
class Mixed(A, UnknownBase): ...
static_assert(is_assignable_to(Intersection[FromAny, Not[int]], A | B))
```
The first arm excludes `Mixed` itself, but its unknown base can still match `B`.
```py
static_assert(is_assignable_to(Intersection[Mixed, Not[int]], Intersection[A, Not[Mixed]] | B))
```
## Heterogeneous tuple types
```py
@@ -349,6 +349,124 @@ def f(
reveal_type(h) # revealed: tuple[Any | str, ...]
```
## Tuple unions with narrowed built-in elements
A tuple alternative is redundant when its element type is covered by another alternative, including
when that element is a built-in type narrowed by an exclusion. The `float` annotation includes
`int`, so it also covers `int & ~bool`:
```py
from ty_extensions import Intersection, Not
def numeric(value: tuple[Intersection[int, Not[bool]]] | tuple[float]):
reveal_type(value) # revealed: tuple[float]
```
## Tuple unions with built-in exclusions
Matching the positive class is not enough to establish redundancy: its exclusions also matter. For
example, a subclass of both `Marker` and `str` can inhabit the first tuple alternative below, but
neither branch of the second:
```py
from ty_extensions import Intersection, Not
class Marker: ...
def different_exclusions(
value: tuple[Intersection[Marker, Not[int]]] | tuple[Intersection[Marker, Not[str]] | bytes],
):
reveal_type(value) # revealed: tuple[Marker & ~int] | tuple[(Marker & ~str) | bytes]
```
## Tuple unions with different exclusions on the same base
`Base & ~Y` covers `Child & ~Y`, so adding the child's tuple leaves the union unchanged. Excluding a
third class, or starting from an unrelated class, leaves an additional tuple in the union.
```py
from ty_extensions import Intersection, Not
class Base: ...
class Child(Base): ...
class X: ...
class Y: ...
class Z: ...
def compare(
flag: bool,
target: tuple[Intersection[Base, Not[X]] | Intersection[Base, Not[Y]]],
covered: tuple[Intersection[Child, Not[Y]]],
different_exclusion: tuple[Intersection[Child, Not[Z]]],
unrelated: tuple[Intersection[Z, Not[X]]],
):
reveal_type(covered if flag else target) # revealed: tuple[(Base & ~X) | (Base & ~Y)]
reveal_type(different_exclusion if flag else target) # revealed: tuple[Child & ~Z] | tuple[(Base & ~X) | (Base & ~Y)]
reveal_type(unrelated if flag else target) # revealed: tuple[Z & ~X] | tuple[(Base & ~X) | (Base & ~Y)]
```
## Tuple unions with multiple inheritance
Either base of `Both` can cover the narrower tuple, depending on which type its element excludes. A
`Left` that excludes `str` can still be an `int` outside `Right`, so its tuple stays in the union.
```py
from ty_extensions import Intersection, Not
class Left: ...
class Right: ...
class Both(Left, Right): ...
def compare(
flag: bool,
target: tuple[Intersection[Left, Not[int]] | Intersection[Right, Not[str]]],
via_left: tuple[Intersection[Both, Not[int]]],
via_right: tuple[Intersection[Both, Not[str]]],
left_excluding_str: tuple[Intersection[Left, Not[str]]],
):
reveal_type(via_left if flag else target) # revealed: tuple[(Left & ~int) | (Right & ~str)]
reveal_type(via_right if flag else target) # revealed: tuple[(Left & ~int) | (Right & ~str)]
reveal_type(left_excluding_str if flag else target) # revealed: tuple[Left & ~str] | tuple[(Left & ~int) | (Right & ~str)]
```
## Tuple unions with a base of `Any`
Inheriting from `Any` preserves the distinction between a known subclass and an unrelated class when
simplifying tuple unions.
```py
from typing import Any
from ty_extensions import Intersection, Not
class Base(Any): ...
class Child(Base): ...
def compare(
unrelated: tuple[Intersection[Base, Not[int]]] | tuple[str | bytes],
inherited: tuple[Intersection[Child, Not[int]]] | tuple[Intersection[Base, Not[int]] | bytes],
):
reveal_type(unrelated) # revealed: tuple[Base & ~int] | tuple[str | bytes]
reveal_type(inherited) # revealed: tuple[(Base & ~int) | bytes]
```
## Tuple unions with a recursive generic base
`Node & ~Excluded` covers `Child & ~Excluded` even when `Node` appears in its own base class
specialization.
```py
from ty_extensions import Intersection, Not
class Node(list["Node"]): ...
class Child(Node): ...
class Excluded: ...
def recursive(
value: tuple[Intersection[Child, Not[Excluded]]] | tuple[Intersection[Node, Not[Excluded]] | str],
):
reveal_type(value) # revealed: tuple[(Node & ~Excluded) | str]
```
## Unions of other generic containers
```toml
@@ -3,10 +3,11 @@ use ruff_python_ast::name::Name;
use ty_module_resolver::KnownModule;
use ty_python_core::{
definition::{Definition, DefinitionKind},
place_table, use_def_map,
place_table, semantic_index, use_def_map,
};
use crate::place::known_module_symbol;
use crate::place::{known_module_symbol, place_from_bindings};
use crate::types::infer::nearest_enclosing_class;
use crate::types::member::class_member;
use crate::types::{
BindingContext, BoundTypeVarInstance, ClassBase, GenericContext, KnownClass, Parameter,
@@ -149,6 +150,71 @@ fn own_model_fields<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> Box
.collect()
}
/// Metadata extracted from a Django model's `Meta` configuration.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)]
struct ModelMetadata {
is_abstract: bool,
}
#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)]
fn model_metadata<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> ModelMetadata {
let env = ProgramEnvironment::from_scope(class.body_scope(db));
// Subclasses do not inherit `abstract = True`, so only inspect this model's own Meta.
let Some(meta) = own_meta(db, class) else {
return ModelMetadata::default();
};
for meta_base in meta.iter_mro(db, None).filter_map(ClassBase::into_class) {
let Some((meta_base, _)) = meta_base.static_class_literal(db) else {
return ModelMetadata::default();
};
// Django resets an abstract model's Meta.abstract to False during model creation.
// A child can inherit other options from that Meta, but must set abstract=True itself.
let enclosing_scope = meta_base.definition(db).scope(db);
if let Some(owner) = nearest_enclosing_class(
db,
semantic_index(db, enclosing_scope.program_file(db)),
enclosing_scope,
) && owner != class
&& is_model(db, owner)
&& own_meta(db, owner) == Some(meta_base)
{
return ModelMetadata::default();
}
let scope = meta_base.body_scope(db);
let Some(symbol) = place_table(db, scope).symbol_id("abstract") else {
continue;
};
// The public type is widened to `bool`, so read the bindings' literal values instead.
let Some(abstract_flag) = place_from_bindings(
db,
&env,
use_def_map(db, scope).end_of_scope_symbol_bindings(symbol),
)
.place
.ignore_possibly_undefined() else {
continue;
};
return ModelMetadata {
is_abstract: abstract_flag == Type::bool_literal(true),
};
}
ModelMetadata::default()
}
fn own_meta<'db>(
db: &'db dyn Db,
class: StaticClassLiteral<'db>,
) -> Option<StaticClassLiteral<'db>> {
let meta = class_member(db, class.body_scope(db), "Meta")
.ignore_possibly_undefined()?
.as_class_literal()?
.as_static()?;
(meta.name(db).as_str() == "Meta" && meta.definition(db).scope(db) == class.body_scope(db))
.then_some(meta)
}
/// Provides synthesized attributes and methods for Django models.
pub(in crate::types) fn synthesized_member<'db>(
db: &'db dyn Db,
@@ -226,6 +292,11 @@ fn primary_key_member<'db>(
}
}
// Abstract models do not have an automatic primary key.
if model_metadata(db, class).is_abstract {
return None;
}
known_module_symbol(db, env, KnownModule::DjangoDbModelsFields, "AutoField")
.place
.ignore_possibly_undefined()?
+27 -29
View File
@@ -54,7 +54,7 @@ enum ComparisonResult<'db> {
/// The comparison allows the operand being constrained to be narrowed to the wrapped type.
///
/// For example, if an object of type `LiteralString` compares equal to an object of type
/// `Literal["foo"]`, the equality branch can safely narrow either operand to `Literal["foo"]`.
/// `Literal["foo"]`, the equality branch narrows the `LiteralString` operand to `Literal["foo"]`.
CanNarrow(Type<'db>),
/// The comparison may evaluate to true or false, depending on runtime values.
@@ -743,10 +743,8 @@ fn evaluate_structural_comparison<'db>(
None => narrow_literal_comparison(
db,
env,
left,
right,
left_literal.kind(),
right_literal.kind(),
left_literal,
right_literal,
operator.condition_expects_equality(branch),
),
}
@@ -1434,30 +1432,22 @@ fn finite_alternatives<'db>(
/// Return a constraint for literal pairs whose equality cannot be decided statically.
///
/// This primarily handles `LiteralString`, which can be constrained by a concrete string literal
/// or a string-valued enum member without having a single statically known runtime value.
/// A concrete string or string-valued enum member can constrain a `LiteralString` operand.
/// In the reverse direction, the unknown string value places no constraint on the concrete operand.
fn narrow_literal_comparison<'db>(
db: &'db dyn Db,
env: &ProgramEnvironment<'db>,
left: Type<'db>,
right: Type<'db>,
left_literal: LiteralValueTypeKind<'db>,
right_literal: LiteralValueTypeKind<'db>,
left: LiteralValueType<'db>,
right: LiteralValueType<'db>,
equality_is_positive: bool,
) -> ComparisonResult<'db> {
match (left_literal, right_literal) {
match (left.kind(), right.kind()) {
(LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::String(_)) => {
ComparisonResult::CanNarrow(right.negate_if(db, env, !equality_is_positive))
}
(LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => {
ComparisonResult::CanNarrow(left.negate_if(db, env, !equality_is_positive))
ComparisonResult::CanNarrow(Type::from(right).negate_if(db, env, !equality_is_positive))
}
(LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::Enum(enum_literal)) => {
narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive)
}
(LiteralValueTypeKind::Enum(enum_literal), LiteralValueTypeKind::LiteralString) => {
narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive)
}
_ => ComparisonResult::Ambiguous,
}
}
@@ -1540,7 +1530,20 @@ fn compare_literal_to_other<'db>(
};
let condition_expects_equality = operator.condition_expects_equality(branch);
// Treat broad builtin types as if only the literal itself can compare equal. This is
let literal_constraint = || {
if !condition_expects_equality {
ComparisonResult::CanNarrow(literal_type.negate(db, env))
} else if matches!(literal, LiteralValueTypeKind::Enum(_))
&& literal_semantics != KnownComparisonSemantics::Object
{
// A plain string can compare equal to a `StrEnum` member.
ComparisonResult::Ambiguous
} else {
ComparisonResult::CanNarrow(literal_type)
}
};
// Treat broad builtin types as if only the builtin literal itself can compare equal. This is
// intentionally unsafe: subclasses, including `bool` for `int`, can compare equal without
// inhabiting the literal type. Explicitly typed subclasses do not take this path.
if evaluator.soundness_policy.allow_unsafe_equality
@@ -1549,7 +1552,7 @@ fn compare_literal_to_other<'db>(
&& let Some(other_semantics) = unsafe_narrowable_builtin_semantics(db, other)
{
return if literal_semantics == other_semantics {
ComparisonResult::CanNarrow(literal_type)
literal_constraint()
} else {
operator.result_from_equality(false)
};
@@ -1574,17 +1577,12 @@ fn compare_literal_to_other<'db>(
if literal_operand == LiteralOperand::Other
&& !other.is_disjoint_from(db, env, literal_type) =>
{
ComparisonResult::CanNarrow(literal_type.negate_if(
db,
env,
!condition_expects_equality,
))
literal_constraint()
}
Some(_) => ComparisonResult::Ambiguous,
None if literal_operand == LiteralOperand::Other && !condition_expects_equality => {
ComparisonResult::CanNarrow(literal_type.negate(db, env))
literal_constraint()
}
None => ComparisonResult::Ambiguous,
_ => ComparisonResult::Ambiguous,
}
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more