Compare commits

..

1 Commits

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

View File

@@ -217,6 +217,11 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "Install Rust toolchain"
run: rustup show
# There are spurious CRL server offline errors when downloading
# `cargo-bloat` with curl below, so we just disable them for now
- name: "Disable SChannel CRL checks"
run: |
reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v EnableCRLCheck /t REG_DWORD /d 0 /f
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:

View File

@@ -1,64 +1,5 @@
# Changelog
## 0.9.5
### Preview features
- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719))
- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837))
- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799))
- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853))
- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821))
- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889))
- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854))
- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762))
- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872))
- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862))
- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905))
- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832))
- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841))
- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922))
- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999))
- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888))
### Rule changes
- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818))
- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838))
- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885))
- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954))
- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840))
- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921))
- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980))
### Configuration
- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918))
- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992))
### Bug fixes
- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877))
- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929))
- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851))
- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988))
- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829))
- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930))
- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953))
- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779))
### Documentation
- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850))
- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882))
- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946))
- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909))
- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844))
- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928))
- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956))
- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982))
- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883))
## 0.9.4
### Preview features

7
Cargo.lock generated
View File

@@ -2525,7 +2525,6 @@ dependencies = [
"regex",
"ruff_db",
"ruff_index",
"ruff_python_ast",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
@@ -2640,7 +2639,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"anyhow",
"argfile",
@@ -2872,7 +2871,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3190,7 +3189,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.5"
version = "0.9.4"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.9.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.5/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.4/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.5
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff

View File

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

View File

@@ -393,6 +393,8 @@ reveal_type(D().x) # revealed: Unknown | Literal[1]
If `staticmethod` is something else, that should not influence the behavior:
`other.py`:
```py
def staticmethod(f):
return f
@@ -407,6 +409,8 @@ reveal_type(C().x) # revealed: Unknown | Literal[1]
And if `staticmethod` is fully qualified, that should also be recognized:
`fully_qualified.py`:
```py
import builtins
@@ -845,6 +849,8 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
functions are instances of that class:
`a.py`:
```py
def f(): ...
@@ -854,7 +860,11 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however:
`b.py`:
```py
def f(): ...
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
```
@@ -864,6 +874,8 @@ reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
integers are instances of that class:
`a.py`:
```py
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).denominator) # revealed: @Todo(@property)
@@ -871,6 +883,8 @@ reveal_type((2).denominator) # revealed: @Todo(@property)
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type((2).numerator) # revealed: Literal[2]
reveal_type((2).real) # revealed: Literal[2]
@@ -881,6 +895,8 @@ reveal_type((2).real) # revealed: Literal[2]
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bols are instances of that class:
`a.py`:
```py
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
@@ -888,6 +904,8 @@ reveal_type(False.__or__) # revealed: @Todo(bound method)
Some attributes are special-cased, however:
`b.py`:
```py
reveal_type(True.numerator) # revealed: Literal[1]
reveal_type(False.real) # revealed: Literal[0]

View File

@@ -33,6 +33,8 @@ reveal_type(a >= b) # revealed: Literal[False]
Even when tuples have different lengths, comparisons should be handled appropriately.
`different_length.py`:
```py
a = (1, 2, 3)
b = (1, 2, 3, 4)
@@ -102,6 +104,8 @@ reveal_type(a >= b) # revealed: bool
However, if the lexicographic comparison completes without reaching a point where str and int are
compared, Python will still produce a result based on the prior elements.
`short_circuit.py`:
```py
a = (1, 2)
b = (999999, "hello")

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
*after* that redefinition.
`union_type_inferred.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -50,7 +52,12 @@ reveal_type(x) # revealed: str | Literal[2]
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
inferred as having a union type following the `try`/`except` block:
`branches_unify_to_non_union_type.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -130,6 +137,8 @@ the `except` suite:
- At the end of `else`, `x == 3`
- At the end of `except`, `x == 2`
`single_except.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -158,6 +167,9 @@ been executed in its entirety, or the `try` suite and the `else` suite must both
in their entireties:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -186,6 +198,8 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
type of `x` at the end of the example is therefore `Literal[2]`:
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -211,7 +225,12 @@ at this point than there were when we were inside the `finally` block.
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
still a TODO item for us.)
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
x = 1
try:
@@ -240,6 +259,8 @@ suites:
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
`except` suite ran to completion
`redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -277,7 +298,18 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
suite.)
`no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
x = 1
try:
@@ -299,7 +331,18 @@ reveal_type(x) # revealed: str | bool
An example with multiple `except` branches and a `finally` branch:
`multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
@@ -337,6 +380,8 @@ If the exception handler has an `else` branch, we must also take into account th
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
an exception raised *there*.
`single_except_branch.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
@@ -380,7 +425,24 @@ reveal_type(x) # revealed: bool | float
The same again, this time with multiple `except` branches:
`multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str:
return "foo"
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_range() -> range:
return range(42)

View File

@@ -218,33 +218,3 @@ import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```
## In the src-root
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from .parser import X
reveal_type(X) # revealed: int
```
## Beyond the src-root
`parser.py`:
```py
X: int = 42
```
`__main__.py`:
```py
from ..parser import X # error: [unresolved-import]
```

View File

@@ -29,6 +29,8 @@ def foo():
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
are excluded:
`unbound_dunders.py`:
```py
# error: [unresolved-reference]
# revealed: Unknown
@@ -70,7 +72,11 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
dynamic imports; but we ignore that for module-literal types where we know exactly which module
we're dealing with:
`__getattr__.py`:
```py
import typing
# error: [unresolved-attribute]
reveal_type(typing.__getattr__) # revealed: Unknown
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,36 +7,43 @@ branches whose conditions we can statically determine to be always true or alway
useful for `sys.version_info` branches, which can make new features available based on the Python
version:
If we can statically determine that the condition is always true, then we can also understand that
`SomeFeature` is always bound, without raising any errors:
`module1.py`:
```py
import sys
class C:
if sys.version_info >= (3, 9):
SomeFeature: str = "available"
if sys.version_info >= (3, 9):
SomeFeature: str = "available"
```
# C.SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
reveal_type(C.SomeFeature) # revealed: str
If we can statically determine that the condition is always true, then we can also understand that
`SomeFeature` is always bound, without raising any errors:
`test1.py`:
```py
from module1 import SomeFeature
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
reveal_type(SomeFeature) # revealed: str
```
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
for conditional imports:
`module.py`:
`module2.py`:
```py
class SomeType: ...
```
`main.py`:
`test2.py`:
```py
import typing
if typing.TYPE_CHECKING:
from module import SomeType
from module2 import SomeType
# `SomeType` is unconditionally available here for type checkers:
def f(s: SomeType) -> None: ...

View File

@@ -37,6 +37,8 @@ child expression now suppresses errors in the outer expression.
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
`"test"` and adding `"other"` to the result of the cast.
`nested.py`:
```py
# fmt: off
from typing import cast

View File

@@ -109,6 +109,8 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
The fields of `sys.version_info` can be accessed by name:
`a.py`:
```py
import sys
@@ -120,7 +122,11 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
properties on instance types:
`b.py`:
```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)

View File

@@ -452,9 +452,6 @@ def raise_in_both_branches(cond: bool):
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
else:
# This branch is unreachable, since all control flows in the `try` clause raise exceptions.
# As a result, this binding should never be reachable, since new bindings are visible only
# when they are reachable.
x = "unreachable"
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
@@ -626,9 +623,9 @@ def return_from_nested_if(cond1: bool, cond2: bool):
## Statically known terminal statements
We model reachability using the same visibility constraints that we use to model statically known
bounds. In this example, we see that the `return` statement is always executed, and therefore that
the `"b"` assignment is not visible to the `reveal_type`.
Terminal statements do not yet interact correctly with statically known bounds. In this example, we
should see that the `return` statement is always executed, and therefore that the `"b"` assignment
is not visible to the `reveal_type`.
```py
def _(cond: bool):
@@ -638,26 +635,6 @@ def _(cond: bool):
if True:
return
reveal_type(x) # revealed: Literal["a"]
```
## Bindings after a terminal statement are unreachable
Any bindings introduced after a terminal statement are unreachable, and are currently considered not
visible. We [anticipate](https://github.com/astral-sh/ruff/issues/15797) that we want to provide a
more useful analysis for code after terminal statements.
```py
def f(cond: bool) -> str:
x = "before"
if cond:
reveal_type(x) # revealed: Literal["before"]
return
x = "after-return"
# TODO: no unresolved-reference error
# error: [unresolved-reference]
reveal_type(x) # revealed: Unknown
else:
x = "else"
reveal_type(x) # revealed: Literal["else"]
# TODO: Literal["a"]
reveal_type(x) # revealed: Literal["a", "b"]
```

View File

@@ -84,11 +84,8 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: tuple[str, Unknown]
reveal_type(z) # revealed: Unknown | Literal[1]
```
`Unknown` can be subclassed, just like `Any`:
```py
# Unknown can be subclassed, just like Any
class C(Unknown): ...
# revealed: tuple[Literal[C], Unknown, Literal[object]]
@@ -241,12 +238,9 @@ error_message = "A custom message "
error_message += "constructed from multiple string literals"
# error: "Static assertion error: A custom message constructed from multiple string literals"
static_assert(False, error_message)
```
There are limitations to what we can still infer as a string literal. In those cases, we simply fall
back to the default message:
```py
# There are limitations to what we can still infer as a string literal. In those cases,
# we simply fall back to the default message.
shouted_message = "A custom message".upper()
# error: "Static assertion error: argument evaluates to `False`"
static_assert(False, shouted_message)
@@ -377,11 +371,8 @@ static_assert(is_subtype_of(TypeOf[str], type[str]))
class Base: ...
class Derived(Base): ...
```
`TypeOf` can also be used in annotations:
```py
# `TypeOf` can be used in annotations:
def type_of_annotation() -> None:
t1: TypeOf[Base] = Base
t2: TypeOf[Base] = Derived # error: [invalid-assignment]

View File

@@ -19,17 +19,11 @@ static_assert(is_equivalent_to(Never, tuple[int, Never]))
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
```
The empty `tuple` is *not* equivalent to `Never`!
```py
# The empty tuple is *not* equivalent to Never!
static_assert(not is_equivalent_to(Never, tuple[()]))
```
`NoReturn` is just a different spelling of `Never`, so the same is true for `NoReturn`:
```py
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))

View File

@@ -793,30 +793,9 @@ where
&mut builder.current_first_parameter_name,
&mut first_parameter_name,
);
// TODO: Fix how we determine the public types of symbols in a
// function-like scope: https://github.com/astral-sh/ruff/issues/15777
//
// In the meantime, visit the function body, but treat the last statement
// specially if it is a return. If it is, this would cause all definitions
// in the function to be marked as non-visible with our current treatment
// of terminal statements. Since we currently model the externally visible
// definitions in a function scope as the set of bindings that are visible
// at the end of the body, we then consider this function to have no
// externally visible definitions. To get around this, we take a flow
// snapshot just before processing the return statement, and use _that_ as
// the "end-of-body" state that we resolve external references against.
if let Some((last_stmt, first_stmts)) = body.split_last() {
builder.visit_body(first_stmts);
let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_))
.then(|| builder.flow_snapshot());
builder.visit_stmt(last_stmt);
if let Some(pre_return_state) = pre_return_state {
builder.flow_restore(pre_return_state);
}
}
builder.visit_body(body);
builder.current_first_parameter_name = first_parameter_name;
builder.pop_scope()
},
);

View File

@@ -478,6 +478,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
scope_start_visibility: ScopedVisibilityConstraintId,
reachable: bool,
}
#[derive(Debug)]
@@ -505,6 +506,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
reachable: bool,
}
impl Default for UseDefMapBuilder<'_> {
@@ -517,13 +520,14 @@ impl Default for UseDefMapBuilder<'_> {
bindings_by_use: IndexVec::new(),
definitions_by_definition: FxHashMap::default(),
symbol_states: IndexVec::new(),
reachable: true,
}
}
}
impl<'db> UseDefMapBuilder<'db> {
pub(super) fn mark_unreachable(&mut self) {
self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE);
self.reachable = false;
}
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
@@ -540,7 +544,7 @@ impl<'db> UseDefMapBuilder<'db> {
binding,
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
);
symbol_state.record_binding(def_id, self.scope_start_visibility);
symbol_state.record_binding(def_id);
}
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
@@ -592,11 +596,7 @@ impl<'db> UseDefMapBuilder<'db> {
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
// If there are any control flow paths that have become unreachable between `snapshot` and
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
if self.scope_start_visibility != snapshot.scope_start_visibility {
return;
}
self.scope_start_visibility = snapshot.scope_start_visibility;
// Note that this loop terminates when we reach a symbol not present in the snapshot.
// This means we keep visibility constraints for all new symbols, which is intended,
@@ -632,7 +632,7 @@ impl<'db> UseDefMapBuilder<'db> {
let def_id = self.all_definitions.push(Some(definition));
let symbol_state = &mut self.symbol_states[symbol];
symbol_state.record_declaration(def_id);
symbol_state.record_binding(def_id, self.scope_start_visibility);
symbol_state.record_binding(def_id);
}
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
@@ -649,6 +649,7 @@ impl<'db> UseDefMapBuilder<'db> {
FlowSnapshot {
symbol_states: self.symbol_states.clone(),
scope_start_visibility: self.scope_start_visibility,
reachable: self.reachable,
}
}
@@ -671,23 +672,21 @@ impl<'db> UseDefMapBuilder<'db> {
num_symbols,
SymbolState::undefined(self.scope_start_visibility),
);
self.reachable = snapshot.reachable;
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
// As an optimization, if we know statically that either of the snapshots is always
// unreachable, we can leave it out of the merged result entirely. Note that we cannot
// perform any type inference at this point, so this is largely limited to unreachability
// via terminal statements. If a flow's reachability depends on an expression in the code,
// we will include the flow in the merged result; the visibility constraints of its
// bindings will include this reachability condition, so that later during type inference,
// we can determine whether any particular binding is non-visible due to unreachability.
if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
// Unreachable snapshots should not be merged: If the current snapshot is unreachable, it
// should be completely overwritten by the snapshot we're merging in. If the other snapshot
// is unreachable, we should return without merging.
if !snapshot.reachable {
return;
}
if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
if !self.reachable {
self.restore(snapshot);
return;
}
@@ -713,6 +712,9 @@ impl<'db> UseDefMapBuilder<'db> {
self.scope_start_visibility = self
.visibility_constraints
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
// Both of the snapshots are reachable, so the merged result is too.
self.reachable = true;
}
pub(super) fn finish(mut self) -> UseDefMap<'db> {

View File

@@ -237,11 +237,7 @@ impl SymbolBindings {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
) {
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
@@ -249,7 +245,8 @@ impl SymbolBindings {
self.constraints.push(Constraints::default());
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
self.visibility_constraints.push(visibility_constraint);
self.visibility_constraints
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
}
/// Add given constraint to all live bindings.
@@ -352,14 +349,9 @@ impl SymbolState {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(
&mut self,
binding_id: ScopedDefinitionId,
visibility_constraint: ScopedVisibilityConstraintId,
) {
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
self.bindings
.record_binding(binding_id, visibility_constraint);
self.bindings.record_binding(binding_id);
}
/// Add given constraint to all live bindings.
@@ -565,10 +557,7 @@ mod tests {
#[test]
fn with() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym.record_binding(ScopedDefinitionId::from_u32(1));
assert_bindings(&sym, &["1<>"]);
}
@@ -576,10 +565,7 @@ mod tests {
#[test]
fn record_constraint() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym.record_binding(ScopedDefinitionId::from_u32(1));
sym.record_constraint(ScopedConstraintId::from_u32(0));
assert_bindings(&sym, &["1<0>"]);
@@ -591,17 +577,11 @@ mod tests {
// merging the same definition with the same constraint keeps the constraint
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1a.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(1),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
sym1a.merge(sym1b, &mut visibility_constraints);
@@ -610,17 +590,11 @@ mod tests {
// merging the same definition with differing constraints drops all constraints
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym2a.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(
ScopedDefinitionId::from_u32(2),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
sym2a.merge(sym1b, &mut visibility_constraints);
@@ -629,10 +603,7 @@ mod tests {
// merging a constrained definition with unbound keeps both
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym3a.record_binding(
ScopedDefinitionId::from_u32(3),
ScopedVisibilityConstraintId::ALWAYS_TRUE,
);
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);

View File

@@ -2513,32 +2513,18 @@ impl<'db> TypeInferenceBuilder<'db> {
.ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
let mut level = level.get();
if module.kind().is_package() {
level = level.saturating_sub(1);
level -= 1;
}
let mut module_name = module.name().clone();
let tail = tail
.map(|tail| ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax))
.transpose()?;
for remaining_dots in (0..level).rev() {
if let Some(parent) = module_name.parent() {
module_name = parent;
} else if remaining_dots == 0 {
// If we reached a search path root and this was the last dot return the tail if any.
// If there's no tail, then we have a relative import that's too deep.
return tail.ok_or(ModuleNameResolutionError::TooManyDots);
} else {
// We're at a search path root. This is a too deep relative import if there's more than
// one dot remaining.
return Err(ModuleNameResolutionError::TooManyDots);
}
for _ in 0..level {
module_name = module_name
.parent()
.ok_or(ModuleNameResolutionError::TooManyDots)?;
}
if let Some(tail) = tail {
let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
module_name.extend(&tail);
}
Ok(module_name)
}

View File

@@ -62,15 +62,10 @@ impl<'db> Unpacker<'db> {
.unwrap_with_diagnostic(&self.context, value.as_any_node_ref(self.db()));
}
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
self.unpack_inner(target, value_ty);
}
fn unpack_inner(
&mut self,
target: &ast::Expr,
value_expr: AnyNodeRef<'db>,
value_ty: Type<'db>,
) {
fn unpack_inner(&mut self, target: &ast::Expr, value_ty: Type<'db>) {
match target {
ast::Expr::Name(target_name) => {
self.targets.insert(
@@ -79,7 +74,7 @@ impl<'db> Unpacker<'db> {
);
}
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
self.unpack_inner(value, value_expr, value_ty);
self.unpack_inner(value, value_ty);
}
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
@@ -158,7 +153,7 @@ impl<'db> Unpacker<'db> {
Type::LiteralString
} else {
ty.iterate(self.db())
.unwrap_with_diagnostic(&self.context, value_expr)
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
};
for target_type in &mut target_types {
target_type.push(ty);
@@ -172,7 +167,7 @@ impl<'db> Unpacker<'db> {
[] => Type::unknown(),
types => UnionType::from_elements(self.db(), types),
};
self.unpack_inner(element, value_expr, element_ty);
self.unpack_inner(element, element_ty);
}
}
_ => {}

View File

@@ -18,7 +18,6 @@ ruff_index = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
camino = { workspace = true }

View File

@@ -20,7 +20,7 @@ reveal_type(1) # revealed: Literal[1]
````
When running this test, the mdtest framework will write a file with these contents to the default
file path (`/src/mdtest_snippet.py`) in its in-memory file system, run a type check on that file,
file path (`/src/mdtest_snippet__1.py`) in its in-memory file system, run a type check on that file,
and then match the resulting diagnostics with the assertions in the test. Assertions are in the form
of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise,
it fails.
@@ -34,8 +34,7 @@ syntax, it's just how this README embeds an example mdtest Markdown document.)
See actual example mdtest suites in
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
> [!NOTE]
> If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
> Note: If you use `dir-test`, `rstest` or similar to generate a separate test for all Markdown files in a certain directory,
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
> Markdown file is added or removed from the directory. See
@@ -127,31 +126,6 @@ Intervening empty lines or non-assertion comments are not allowed; an assertion
assertion per line, immediately following each other, with the line immediately following the last
assertion as the line of source code on which the matched diagnostics are emitted.
## Literate style
If multiple code blocks (without an explicit path, see below) are present in a single test, they will
be merged into a single file in the order they appear in the Markdown file. This allows for tests that
interleave code and explanations:
````markdown
# My literate test
This first snippet here:
```py
from typing import Literal
def f(x: Literal[1]):
pass
```
will be merged with this second snippet here, i.e. `f` is defined here:
```py
f(2) # error: [invalid-argument-type]
```
````
## Diagnostic Snapshotting
In addition to inline assertions, one can also snapshot the full diagnostic
@@ -182,8 +156,13 @@ snapshotting of specific diagnostics.
## Multi-file tests
Some tests require multiple files, with imports from one file into another. For this purpose,
tests can specify explicit file paths in a separate line before the code block (`b.py` below):
Some tests require multiple files, with imports from one file into another. Multiple fenced code
blocks represent multiple embedded files. If there are multiple unnamed files, mdtest will name them
according to the numbered scheme `/src/mdtest_snippet__1.py`, `/src/mdtest_snippet__2.py`, etc. (If
they are `pyi` files, they will be named with a `pyi` extension instead.)
Tests should not rely on these default names. If a test must import from a file, then it should
explicitly specify the file name:
````markdown
```py
@@ -204,8 +183,8 @@ is, the equivalent of a runtime entry on `sys.path`).
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
this is a feature we will want to add in the future.
So the above test creates two files, `/src/mdtest_snippet.py` and `/src/b.py`, and sets the workspace
root to `/src/`, allowing imports from `b.py` using the module name `b`.
So the above test creates two files, `/src/mdtest_snippet__1.py` and `/src/b.py`, and sets the
workspace root to `/src/`, allowing imports from `b.py` using the module name `b`.
## Multi-test suites
@@ -419,7 +398,7 @@ This is just an example, not a proposal that red-knot would ever actually output
precisely this format:
```output
mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]'
mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
```
````
@@ -427,7 +406,7 @@ We will want to build tooling to automatically capture and update these “full
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
By default, an `output` block will specify diagnostic output for the file
`<workspace-root>/mdtest_snippet.py`. An `output` block can be prefixed by a
`<workspace-root>/mdtest_snippet__1.py`. An `output` block can be prefixed by a
<code>`&lt;path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
diagnostic output.
@@ -463,7 +442,7 @@ x = 1
Initial expected output for the unnamed file:
```output
/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[1]'
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
```
Now in our first incremental stage, modify the contents of `b.py`:
@@ -478,12 +457,12 @@ x = 2
And this is our updated expected output for the unnamed file at stage 1:
```output stage=1
/src/mdtest_snippet.py, line 1, col 1: revealed type is 'Literal[2]'
/src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[2]'
```
(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment
diagnostic assertions for `mdtest_snippet.py` would require specifying new contents for
`mdtest_snippet.py` in stage 1, which we don't want to do in this test.)
diagnostic assertions for `mdtest_snippet__1.py` would require specifying new contents for
`mdtest_snippet__1.py` in stage 1, which we don't want to do in this test.)
````
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies

View File

@@ -1,5 +1,4 @@
use crate::config::Log;
use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap};
use camino::Utf8Path;
use colored::Colorize;
use parser as test_parser;
@@ -12,6 +11,7 @@ use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextSize;
use std::fmt::Write;
mod assertion;
@@ -67,14 +67,12 @@ pub fn run(
let md_index = LineIndex::from_source_text(&source);
for test_failures in failures {
let source_map =
EmbeddedFileSourceMap::new(&md_index, test_failures.backtick_offsets);
let backtick_line = md_index.line_index(test_failures.backtick_offset);
for (relative_line_number, failures) in test_failures.by_line.iter() {
let absolute_line_number =
source_map.to_absolute_line_number(relative_line_number);
for failure in failures {
let absolute_line_number =
backtick_line.checked_add(relative_line_number).unwrap();
let line_info =
format!("{relative_fixture_path}:{absolute_line_number}").cyan();
println!(" {line_info} {failure}");
@@ -122,7 +120,11 @@ fn run_test(
"Supported file types are: py, pyi, text"
);
let full_path = embedded.full_path(&project_root);
let full_path = if embedded.path.starts_with('/') {
SystemPathBuf::from(embedded.path.clone())
} else {
project_root.join(&embedded.path)
};
if let Some(ref typeshed_path) = custom_typeshed_path {
if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) {
@@ -134,7 +136,7 @@ fn run_test(
}
}
db.write_file(&full_path, &embedded.code).unwrap();
db.write_file(&full_path, embedded.code).unwrap();
if !full_path.starts_with(&src_path) || embedded.lang == "text" {
// These files need to be written to the file system (above), but we don't run any checks on them.
@@ -145,7 +147,7 @@ fn run_test(
Some(TestFile {
file,
backtick_offsets: embedded.backtick_offsets.clone(),
backtick_offset: embedded.backtick_offset,
})
})
.collect();
@@ -228,7 +230,7 @@ fn run_test(
}
by_line.push(OneIndexed::from_zero_indexed(0), messages);
return Some(FileFailures {
backtick_offsets: test_file.backtick_offsets,
backtick_offset: test_file.backtick_offset,
by_line,
});
}
@@ -242,7 +244,7 @@ fn run_test(
match matcher::match_file(db, test_file.file, diagnostics.iter().map(|d| &**d)) {
Ok(()) => None,
Err(line_failures) => Some(FileFailures {
backtick_offsets: test_file.backtick_offsets,
backtick_offset: test_file.backtick_offset,
by_line: line_failures,
}),
};
@@ -278,11 +280,11 @@ fn run_test(
type Failures = Vec<FileFailures>;
/// The failures for a single file in a test by line number.
#[derive(Debug)]
struct FileFailures {
/// Positional information about the code block(s) to reconstruct absolute line numbers.
backtick_offsets: Vec<BacktickOffsets>,
/// The failures by lines in the file.
/// The offset of the backticks that starts the code block in the Markdown file
backtick_offset: TextSize,
/// The failures by lines in the code block.
by_line: matcher::FailuresByLine,
}
@@ -290,8 +292,8 @@ struct FileFailures {
struct TestFile {
file: File,
/// Positional information about the code block(s) to reconstruct absolute line numbers.
backtick_offsets: Vec<BacktickOffsets>,
// Offset of the backticks that starts the code block in the Markdown file
backtick_offset: TextSize,
}
fn create_diagnostic_snapshot<D: Diagnostic>(
@@ -315,7 +317,7 @@ fn create_diagnostic_snapshot<D: Diagnostic>(
writeln!(snapshot, "# Python source files").unwrap();
writeln!(snapshot).unwrap();
for file in test.files() {
writeln!(snapshot, "## {}", file.relative_path()).unwrap();
writeln!(snapshot, "## {}", file.path).unwrap();
writeln!(snapshot).unwrap();
// Note that we don't use ```py here because the line numbering
// we add makes it invalid Python. This sacrifices syntax

View File

@@ -1,13 +1,9 @@
use std::{borrow::Cow, collections::hash_map::Entry};
use anyhow::bail;
use ruff_db::system::{SystemPath, SystemPathBuf};
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_index::{newtype_index, IndexVec};
use ruff_python_ast::PySourceType;
use ruff_python_trivia::Cursor;
use ruff_source_file::{LineIndex, LineRanges, OneIndexed};
use ruff_source_file::LineRanges;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::config::MarkdownTestConfig;
@@ -136,112 +132,6 @@ struct Section<'s> {
#[newtype_index]
struct EmbeddedFileId;
/// Holds information about the start and the end of a code block in a Markdown file.
///
/// The start is the offset of the first triple-backtick in the code block, and the end is the
/// offset of the (start of the) closing triple-backtick.
#[derive(Debug, Clone)]
pub(crate) struct BacktickOffsets(TextSize, TextSize);
/// Holds information about the position and length of all code blocks that are part of
/// a single embedded file in a Markdown file. This is used to reconstruct absolute line
/// numbers (in the Markdown file) from relative line numbers (in the embedded file).
///
/// If we have a Markdown section with multiple code blocks like this:
///
/// 01 # Test
/// 02
/// 03 Part 1:
/// 04
/// 05 ```py
/// 06 a = 1 # Relative line number: 1
/// 07 b = 2 # Relative line number: 2
/// 08 ```
/// 09
/// 10 Part 2:
/// 11
/// 12 ```py
/// 13 c = 3 # Relative line number: 3
/// 14 ```
///
/// We want to reconstruct the absolute line number (left) from relative
/// line numbers. The information we have is the start line and the line
/// count of each code block:
///
/// - Block 1: (start = 5, count = 2)
/// - Block 2: (start = 12, count = 1)
///
/// For example, if we see a relative line number of 3, we see that it is
/// larger than the line count of the first block, so we subtract the line
/// count of the first block, and then add the new relative line number (1)
/// to the absolute start line of the second block (12), resulting in an
/// absolute line number of 13.
pub(crate) struct EmbeddedFileSourceMap {
start_line_and_line_count: Vec<(usize, usize)>,
}
impl EmbeddedFileSourceMap {
pub(crate) fn new(
md_index: &LineIndex,
dimensions: impl IntoIterator<Item = BacktickOffsets>,
) -> EmbeddedFileSourceMap {
EmbeddedFileSourceMap {
start_line_and_line_count: dimensions
.into_iter()
.map(|d| {
let start_line = md_index.line_index(d.0).get();
let end_line = md_index.line_index(d.1).get();
let code_line_count = (end_line - start_line) - 1;
(start_line, code_line_count)
})
.collect(),
}
}
pub(crate) fn to_absolute_line_number(&self, relative_line_number: OneIndexed) -> OneIndexed {
let mut absolute_line_number = 0;
let mut relative_line_number = relative_line_number.get();
for (start_line, line_count) in &self.start_line_and_line_count {
if relative_line_number > *line_count {
relative_line_number -= *line_count;
} else {
absolute_line_number = start_line + relative_line_number;
break;
}
}
OneIndexed::new(absolute_line_number).expect("Relative line number out of bounds")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum EmbeddedFilePath<'s> {
Autogenerated(PySourceType),
Explicit(&'s str),
}
impl EmbeddedFilePath<'_> {
pub(crate) fn as_str(&self) -> &str {
match self {
EmbeddedFilePath::Autogenerated(PySourceType::Python) => "mdtest_snippet.py",
EmbeddedFilePath::Autogenerated(PySourceType::Stub) => "mdtest_snippet.pyi",
EmbeddedFilePath::Autogenerated(PySourceType::Ipynb) => "mdtest_snippet.ipynb",
EmbeddedFilePath::Explicit(path) => path,
}
}
fn is_explicit(&self) -> bool {
matches!(self, EmbeddedFilePath::Explicit(_))
}
fn is_allowed_explicit_path(path: &str) -> bool {
[PySourceType::Python, PySourceType::Stub]
.iter()
.all(|source_type| path != EmbeddedFilePath::Autogenerated(*source_type).as_str())
}
}
/// A single file embedded in a [`Section`] as a fenced code block.
///
/// Currently must be a Python file (`py` language), a type stub (`pyi`) or a [typeshed `VERSIONS`]
@@ -258,39 +148,12 @@ impl EmbeddedFilePath<'_> {
#[derive(Debug)]
pub(crate) struct EmbeddedFile<'s> {
section: SectionId,
path: EmbeddedFilePath<'s>,
pub(crate) path: String,
pub(crate) lang: &'s str,
pub(crate) code: Cow<'s, str>,
pub(crate) backtick_offsets: Vec<BacktickOffsets>,
}
pub(crate) code: &'s str,
impl EmbeddedFile<'_> {
fn append_code(&mut self, backtick_offsets: BacktickOffsets, new_code: &str) {
// Treat empty code blocks as non-existent, instead of creating
// an additional empty line:
if new_code.is_empty() {
return;
}
self.backtick_offsets.push(backtick_offsets);
let existing_code = self.code.to_mut();
existing_code.push('\n');
existing_code.push_str(new_code);
}
pub(crate) fn relative_path(&self) -> &str {
self.path.as_str()
}
pub(crate) fn full_path(&self, project_root: &SystemPath) -> SystemPathBuf {
let relative_path = self.relative_path();
if relative_path.starts_with('/') {
SystemPathBuf::from(relative_path)
} else {
project_root.join(relative_path)
}
}
/// The offset of the backticks beginning the code block within the markdown file
pub(crate) backtick_offset: TextSize,
}
#[derive(Debug)]
@@ -332,6 +195,12 @@ struct Parser<'s> {
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
/// The counts are done by section. This gives each code block a
/// somewhat locally derived name. That is, adding new sections
/// won't change the names of files in other sections. This is
/// important for avoiding snapshot churn.
unnamed_file_count: FxHashMap<SectionId, usize>,
/// The unparsed remainder of the Markdown source.
cursor: Cursor<'s>,
@@ -348,7 +217,7 @@ struct Parser<'s> {
stack: SectionStack,
/// Names of embedded files in current active section.
current_section_files: FxHashMap<EmbeddedFilePath<'s>, EmbeddedFileId>,
current_section_files: Option<FxHashSet<String>>,
/// Whether or not the current section has a config block.
current_section_has_config: bool,
@@ -368,12 +237,13 @@ impl<'s> Parser<'s> {
sections,
source,
files: IndexVec::default(),
unnamed_file_count: FxHashMap::default(),
cursor: Cursor::new(source),
preceding_blank_lines: 0,
explicit_path: None,
source_len: source.text_len(),
stack: SectionStack::new(root_section_id),
current_section_files: FxHashMap::default(),
current_section_files: None,
current_section_has_config: false,
}
}
@@ -464,7 +334,7 @@ impl<'s> Parser<'s> {
if self.cursor.eat_char2('`', '`') {
// We saw the triple-backtick beginning of a code block.
let backtick_offset_start = self.offset() - "```".text_len();
let backtick_offset = self.offset() - "```".text_len();
if self.preceding_blank_lines < 1 && self.explicit_path.is_none() {
bail!("Code blocks must start on a new line and be preceded by at least one blank line.");
@@ -493,13 +363,7 @@ impl<'s> Parser<'s> {
code = &code[..code.len() - '\n'.len_utf8()];
}
let backtick_offset_end = self.offset() - "```".text_len();
self.process_code_block(
lang,
code,
BacktickOffsets(backtick_offset_start, backtick_offset_end),
)?;
self.process_code_block(lang, code, backtick_offset)?;
} else {
let code_block_start = self.cursor.token_len();
let line = self.source.count_lines(TextRange::up_to(code_block_start));
@@ -564,7 +428,7 @@ impl<'s> Parser<'s> {
snapshot_diagnostics: self.sections[parent].snapshot_diagnostics,
};
if !self.current_section_files.is_empty() {
if self.current_section_files.is_some() {
bail!(
"Header '{}' not valid inside a test case; parent '{}' has code files.",
section.title,
@@ -575,7 +439,7 @@ impl<'s> Parser<'s> {
let section_id = self.sections.push(section);
self.stack.push(section_id);
self.current_section_files.clear();
self.current_section_files = None;
self.current_section_has_config = false;
Ok(())
@@ -585,11 +449,10 @@ impl<'s> Parser<'s> {
&mut self,
lang: &'s str,
code: &'s str,
backtick_offsets: BacktickOffsets,
backtick_offset: TextSize,
) -> anyhow::Result<()> {
// We never pop the implicit root section.
let section = self.stack.top();
let test_name = self.sections[section].title;
if lang == "toml" {
return self.process_config_block(code);
@@ -602,86 +465,52 @@ impl<'s> Parser<'s> {
&& !explicit_path.ends_with(&format!(".{lang}"))
{
bail!(
"File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block"
"File ending of test file path `{explicit_path}` does not match `lang={lang}` of code block"
);
}
}
let path = match self.explicit_path {
Some(path) => {
if !EmbeddedFilePath::is_allowed_explicit_path(path) {
bail!(
"The file name `{path}` in test `{test_name}` must not be used explicitly.",
);
}
Some(path) => path.to_string(),
None => {
let unnamed_file_count = self.unnamed_file_count.entry(section).or_default();
*unnamed_file_count += 1;
EmbeddedFilePath::Explicit(path)
match lang {
"py" | "pyi" => format!("mdtest_snippet__{unnamed_file_count}.{lang}"),
"" => format!("mdtest_snippet__{unnamed_file_count}.py"),
_ => {
bail!(
"Cannot generate name for `lang={}`: Unsupported extension",
lang
);
}
}
}
None => match lang {
"py" => EmbeddedFilePath::Autogenerated(PySourceType::Python),
"pyi" => EmbeddedFilePath::Autogenerated(PySourceType::Stub),
"" => {
bail!("Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`");
}
_ => {
bail!(
"Cannot auto-generate file name for code block with language `{}` in test `{test_name}`",
lang
);
}
},
};
let has_merged_snippets = self.current_section_has_merged_snippets();
let has_explicit_file_paths = self.current_section_has_explicit_file_paths();
self.files.push(EmbeddedFile {
path: path.clone(),
section,
lang,
code,
backtick_offset,
});
match self.current_section_files.entry(path.clone()) {
Entry::Vacant(entry) => {
if has_merged_snippets {
bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files.");
}
let index = self.files.push(EmbeddedFile {
path: path.clone(),
section,
lang,
code: Cow::Borrowed(code),
backtick_offsets: vec![backtick_offsets],
});
entry.insert(index);
}
Entry::Occupied(entry) => {
if path.is_explicit() {
bail!(
"Test `{test_name}` has duplicate files named `{}`.",
path.as_str(),
);
};
if has_explicit_file_paths {
bail!("Merged snippets in test `{test_name}` are not allowed in the presence of other files.");
}
let index = *entry.get();
self.files[index].append_code(backtick_offsets, code);
}
if let Some(current_files) = &mut self.current_section_files {
if !current_files.insert(path.clone()) {
bail!(
"Test `{}` has duplicate files named `{path}`.",
self.sections[section].title
);
};
} else {
self.current_section_files = Some(FxHashSet::from_iter([path]));
}
Ok(())
}
fn current_section_has_explicit_file_paths(&self) -> bool {
self.current_section_files
.iter()
.any(|(path, _)| path.is_explicit())
}
fn current_section_has_merged_snippets(&self) -> bool {
self.current_section_files
.values()
.any(|id| self.files[*id].backtick_offsets.len() > 1)
}
fn process_config_block(&mut self, code: &str) -> anyhow::Result<()> {
if self.current_section_has_config {
bail!("Multiple TOML configuration blocks in the same section are not allowed.");
@@ -702,7 +531,7 @@ impl<'s> Parser<'s> {
everything else (including TOML configuration blocks).",
);
}
if !self.current_section_files.is_empty() {
if self.current_section_files.is_some() {
bail!(
"Section config to enable snapshotting diagnostics must come before \
everything else (including embedded files).",
@@ -726,7 +555,7 @@ impl<'s> Parser<'s> {
self.stack.pop();
// We would have errored before pushing a child section if there were files, so we know
// no parent section can have files.
self.current_section_files.clear();
self.current_section_files = None;
}
}
@@ -738,11 +567,8 @@ impl<'s> Parser<'s> {
#[cfg(test)]
mod tests {
use ruff_python_ast::PySourceType;
use ruff_python_trivia::textwrap::dedent;
use crate::parser::EmbeddedFilePath;
#[test]
fn empty() {
let mf = super::parse("file.md", "").unwrap();
@@ -771,10 +597,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
}
@@ -799,10 +622,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
}
@@ -825,14 +645,10 @@ mod tests {
# Three
`mod_a.pyi`:
```pyi
a: int
```
`mod_b.pyi`:
```pyi
b: str
```
@@ -852,10 +668,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
@@ -863,10 +676,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "y = 2");
@@ -874,11 +684,11 @@ mod tests {
panic!("expected two files");
};
assert_eq!(file_1.relative_path(), "mod_a.pyi");
assert_eq!(file_1.path, "mdtest_snippet__1.pyi");
assert_eq!(file_1.lang, "pyi");
assert_eq!(file_1.code, "a: int");
assert_eq!(file_2.relative_path(), "mod_b.pyi");
assert_eq!(file_2.path, "mdtest_snippet__2.pyi");
assert_eq!(file_2.lang, "pyi");
assert_eq!(file_2.code, "b: str");
}
@@ -921,11 +731,11 @@ mod tests {
panic!("expected two files");
};
assert_eq!(main.relative_path(), "main.py");
assert_eq!(main.path, "main.py");
assert_eq!(main.lang, "py");
assert_eq!(main.code, "from foo import y");
assert_eq!(foo.relative_path(), "foo.py");
assert_eq!(foo.path, "foo.py");
assert_eq!(foo.lang, "py");
assert_eq!(foo.code, "y = 2");
@@ -933,157 +743,11 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "y = 2");
}
#[test]
fn merged_snippets() {
let source = dedent(
"
# One
This is the first part of the embedded file:
```py
x = 1
```
And this is the second part:
```py
y = 2
```
And this is the third part:
```py
z = 3
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1\ny = 2\nz = 3");
}
#[test]
fn no_merged_snippets_for_explicit_paths() {
let source = dedent(
"
# One
`foo.py`:
```py
x = 1
```
`foo.py`:
```py
y = 2
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Test `One` has duplicate files named `foo.py`."
);
}
#[test]
fn disallow_merged_snippets_in_presence_of_explicit_paths() {
for source in [
// Merged snippets first
"
# One
```py
x = 1
```
```py
y = 2
```
`foo.py`:
```py
print('hello')
```
",
// Explicit path first
"
# One
`foo.py`:
```py
print('hello')
```
```py
x = 1
```
```py
y = 2
```
",
] {
let err = super::parse("file.md", &dedent(source)).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Merged snippets in test `One` are not allowed in the presence of other files."
);
}
}
#[test]
fn disallow_pyi_snippets_in_presence_of_merged_py_snippets() {
let source = dedent(
"
# One
```py
x = 1
```
```py
y = 2
```
```pyi
x: int
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Merged snippets in test `One` are not allowed in the presence of other files."
);
}
#[test]
fn custom_file_path() {
let source = dedent(
@@ -1104,7 +768,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1");
}
@@ -1156,27 +820,28 @@ mod tests {
fn no_lang() {
let source = dedent(
"
# No language specifier
```
x = 10
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Cannot auto-generate file name for code block with empty language specifier in test `No language specifier`"
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.code, "x = 10");
}
#[test]
fn cannot_generate_name_for_lang() {
let source = dedent(
"
# JSON test?
```json
{}
```
@@ -1185,7 +850,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Cannot auto-generate file name for code block with language `json` in test `JSON test?`"
"Cannot generate name for `lang=json`: Unsupported extension"
);
}
@@ -1193,8 +858,6 @@ mod tests {
fn mismatching_lang() {
let source = dedent(
"
# Accidental stub
`a.py`:
```pyi
@@ -1205,7 +868,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block"
"File ending of test file path `a.py` does not match `lang=pyi` of code block"
);
}
@@ -1230,7 +893,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "lorem");
assert_eq!(file.path, "lorem");
assert_eq!(file.code, "x = 1");
}
@@ -1255,7 +918,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "lorem.yaml");
assert_eq!(file.path, "lorem.yaml");
assert_eq!(file.code, "x = 1");
}
@@ -1277,13 +940,13 @@ mod tests {
"
## A well-fenced block
```py
```
y = 2
```
## A not-so-well-fenced block
```py
```
x = 1
",
);
@@ -1324,8 +987,7 @@ mod tests {
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Indented code blocks are not supported.");
super::parse("file.md", &source).expect_err("Indented code blocks are not supported.");
}
#[test]
@@ -1371,10 +1033,7 @@ mod tests {
};
assert_eq!(test.section.title, "file.md");
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1403,10 +1062,7 @@ mod tests {
};
assert_eq!(test.section.title, "Foo");
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1435,12 +1091,10 @@ mod tests {
}
#[test]
fn no_usage_of_autogenerated_name() {
fn no_duplicate_name_files_in_test_2() {
let source = dedent(
"
# Name clash
`mdtest_snippet.py`:
`mdtest_snippet__1.py`:
```py
x = 1
@@ -1454,7 +1108,7 @@ mod tests {
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"The file name `mdtest_snippet.py` in test `Name clash` must not be used explicitly."
"Test `file.md` has duplicate files named `mdtest_snippet__1.py`."
);
}
@@ -1479,7 +1133,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
@@ -1504,7 +1158,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
@@ -1528,7 +1182,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo.py");
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
@@ -1553,7 +1207,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(file.relative_path(), "foo bar.py");
assert_eq!(file.path, "foo bar.py");
assert_eq!(file.code, "x = 1");
}
@@ -1579,10 +1233,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1607,10 +1258,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1636,10 +1284,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
@@ -1665,10 +1310,7 @@ mod tests {
panic!("expected one file");
};
assert_eq!(
file.path,
EmbeddedFilePath::Autogenerated(PySourceType::Python)
);
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.9.5"
version = "0.9.4"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -2218,11 +2218,13 @@ def func(t: _T) -> _T:
return x
"#
),
@r"
success: true
exit_code: 0
@r#"
success: false
exit_code: 1
----- stdout -----
from typing import TypeVar
_T = TypeVar("_T")
class OldStyle[T]:
var: T
@@ -2232,7 +2234,8 @@ def func(t: _T) -> _T:
return x
----- stderr -----
Found 7 errors (7 fixed, 0 remaining).
"
test.py:3:1: PYI018 Private TypeVar `_T` is never used
Found 6 errors (5 fixed, 1 remaining).
"#
);
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.9.5"
version = "0.9.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -6,10 +6,6 @@ class _bad:
pass
class __bad:
pass
class bad_class:
pass
@@ -17,8 +13,6 @@ class bad_class:
class Bad_Class:
pass
class Bad__Class:
pass
class BAD_CLASS:
pass
@@ -38,6 +32,3 @@ class GoodClass:
class GOOD:
pass
class __GoodClass:
pass

View File

@@ -1,7 +0,0 @@
import os
import sys
sys.path += [os.path.dirname(__file__)]
sys.path += ["../"]
from package import module

View File

@@ -1,109 +0,0 @@
from typing import Generic, ParamSpec, TypeVar, TypeVarTuple, Unpack
_A = TypeVar('_A')
_B = TypeVar('_B', bound=int)
_C = TypeVar('_C', str, bytes)
_D = TypeVar('_D', default=int)
_E = TypeVar('_E', bound=int, default=int)
_F = TypeVar('_F', str, bytes, default=str)
_G = TypeVar('_G', str, a := int)
_As = TypeVarTuple('_As')
_Bs = TypeVarTuple('_Bs', bound=tuple[int, str])
_Cs = TypeVarTuple('_Cs', default=tuple[int, str])
_P1 = ParamSpec('_P1')
_P2 = ParamSpec('_P2', bound=[bytes, bool])
_P3 = ParamSpec('_P3', default=[int, str])
### Errors
class C[T](Generic[_A]): ...
class C[T](Generic[_B], str): ...
class C[T](int, Generic[_C]): ...
class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
class C[*Ts](Generic[*_As]): ...
class C[*Ts](Generic[Unpack[_As]]): ...
class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
class C[**P](Generic[_P1]): ...
class C[**P](Generic[_P2]): ...
class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
class C[T](Generic[T, _A]): ...
# See `is_existing_param_of_same_class`.
# `expr_name_to_type_var` doesn't handle named expressions,
# only simple assignments, so there is no fix.
class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ...
class C(Generic[_B]):
class D[T](Generic[_B, T]): ...
class C[T]:
class D[U](Generic[T, U]): ...
# In a single run, only the first is reported.
# Others will be reported/fixed in following iterations.
class C[T](Generic[_C], Generic[_D]): ...
class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
class C[
T # Comment
](Generic[_E]): ... # TODO: Type parameter defaults
class C[T](Generic[Generic[_F]]): ...
class C[T](Generic[Unpack[_A]]): ...
class C[T](Generic[Unpack[_P1]]): ...
class C[T](Generic[Unpack[Unpack[_P2]]]): ...
class C[T](Generic[Unpack[*_As]]): ...
class C[T](Generic[Unpack[_As, _Bs]]): ...
class C[T](Generic[_A, _A]): ...
class C[T](Generic[_A, Unpack[_As]]): ...
class C[T](Generic[*_As, _A]): ...
from somewhere import APublicTypeVar
class C[T](Generic[APublicTypeVar]): ...
class C[T](Generic[APublicTypeVar, _A]): ...
# `_G` has two constraints: `str` and `a := int`.
# The latter cannot be used as a PEP 695 constraint,
# as named expressions are forbidden within type parameter lists.
# See also the `_Z` example above.
class C[T](Generic[_G]): ... # Should be moved down below eventually
# Single-element constraints should not be converted to a bound.
class C[T: (str,)](Generic[_A]): ...
class C[T: [a]](Generic[_A]): ...
# Existing bounds should not be deparenthesized.
# class C[T: (_Y := int)](Generic[_A]): ... # TODO: Uncomment this
# class C[T: (*a,)](Generic[_A]): ... # TODO: Uncomment this
### No errors
class C(Generic[_A]): ...
class C[_A]: ...
class C[_A](list[_A]): ...
class C[_A](list[Generic[_A]]): ...

View File

@@ -554,9 +554,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695GenericClass) {
pyupgrade::rules::non_pep695_generic_class(checker, class_def);
}
if checker.enabled(Rule::ClassWithMixedTypeVars) {
ruff::rules::class_with_mixed_type_vars(checker, class_def);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) {

View File

@@ -248,11 +248,6 @@ impl<'a> Checker<'a> {
cell_offsets: Option<&'a CellOffsets>,
notebook_index: Option<&'a NotebookIndex>,
) -> Checker<'a> {
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module);
if settings.preview.is_enabled() {
// Set the feature flag to test `TYPE_CHECKING` semantic changes
semantic.flags |= SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION;
}
Self {
parsed,
parsed_type_annotation: None,
@@ -268,7 +263,7 @@ impl<'a> Checker<'a> {
stylist,
indexer,
importer: Importer::new(parsed, locator, stylist),
semantic,
semantic: SemanticModel::new(&settings.typing_modules, path, module),
visit: deferred::Visit::default(),
analyze: deferred::Analyze::default(),
diagnostics: Vec::default(),

View File

@@ -1005,7 +1005,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum),
(Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel),
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),

View File

@@ -9,7 +9,7 @@ use anyhow::Result;
use libcst_native::{ImportAlias, Name as cstName, NameOrAttribute};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
use ruff_python_ast::{self as ast, ModModule, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_parser::{Parsed, Tokens};
use ruff_python_semantic::{
@@ -125,7 +125,7 @@ impl<'a> Importer<'a> {
&self,
import: &ImportedMembers,
at: TextSize,
semantic: &SemanticModel<'a>,
semantic: &SemanticModel,
) -> Result<TypingImportEdit> {
// Generate the modified import statement.
let content = fix::codemods::retain_imports(
@@ -135,39 +135,6 @@ impl<'a> Importer<'a> {
self.stylist,
)?;
// Add the import to an existing `TYPE_CHECKING` block.
if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the existing `TYPE_CHECKING` block.
let type_checking_edit =
if let Some(statement) = Self::type_checking_binding_statement(semantic, block) {
if statement == import.statement {
// Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
// statement that we're modifying, avoid adding a no-op edit. For example, here,
// the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
// from the import:
// ```python
// from __future__ import annotations
//
// from typing import Final, TYPE_CHECKING
//
// Const: Final[dict] = {}
// ```
None
} else {
Some(Edit::range_replacement(
self.locator.slice(statement.range()).to_string(),
statement.range(),
))
}
} else {
None
};
return Ok(TypingImportEdit {
type_checking_edit,
add_import_edit: self.add_to_type_checking_block(&content, block.start()),
});
}
// Import the `TYPE_CHECKING` symbol from the typing module.
let (type_checking_edit, type_checking) =
if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
@@ -212,10 +179,13 @@ impl<'a> Importer<'a> {
(Some(edit), name)
};
// Add the import to a new `TYPE_CHECKING` block.
Ok(TypingImportEdit {
type_checking_edit,
add_import_edit: self.add_type_checking_block(
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the `TYPE_CHECKING` block.
self.add_to_type_checking_block(&content, block.start())
} else {
// Add the import to a new `TYPE_CHECKING` block.
self.add_type_checking_block(
&format!(
"{}if {type_checking}:{}{}",
self.stylist.line_ending().as_str(),
@@ -223,25 +193,13 @@ impl<'a> Importer<'a> {
indent(&content, self.stylist.indentation())
),
at,
)?,
})
}
fn type_checking_binding_statement(
semantic: &SemanticModel<'a>,
type_checking_block: &Stmt,
) -> Option<&'a Stmt> {
let Stmt::If(ast::StmtIf { test, .. }) = type_checking_block else {
return None;
)?
};
let mut source = test;
while let Expr::Attribute(ast::ExprAttribute { value, .. }) = source.as_ref() {
source = value;
}
semantic
.binding(semantic.resolve_name(source.as_name_expr()?)?)
.statement(semantic)
Ok(TypingImportEdit {
type_checking_edit,
add_import_edit,
})
}
/// Find a reference to `typing.TYPE_CHECKING`.

View File

@@ -195,8 +195,6 @@ mod tests {
}
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.py"))]
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.pyi"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::{self as ast, Expr, Stmt};
@@ -6,7 +6,6 @@ use ruff_python_semantic::{Scope, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix;
/// ## What it does
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
@@ -14,8 +13,7 @@ use crate::fix;
///
/// ## Why is this bad?
/// A private `TypeVar` that is defined but not used is likely a mistake. It
/// should either be used, made public, or removed to avoid confusion. A type
/// variable is considered "private" if its name starts with an underscore.
/// should either be used, made public, or removed to avoid confusion.
///
/// ## Example
/// ```pyi
@@ -25,11 +23,6 @@ use crate::fix;
/// _T = typing.TypeVar("_T")
/// _Ts = typing_extensions.TypeVarTuple("_Ts")
/// ```
///
/// ## Fix safety and availability
/// This rule's fix is available when [`preview`] mode is enabled.
/// It is always marked as unsafe, as it would break your code if the type
/// variable is imported by another module.
#[derive(ViolationMetadata)]
pub(crate) struct UnusedPrivateTypeVar {
type_var_like_name: String,
@@ -37,8 +30,6 @@ pub(crate) struct UnusedPrivateTypeVar {
}
impl Violation for UnusedPrivateTypeVar {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let UnusedPrivateTypeVar {
@@ -47,16 +38,6 @@ impl Violation for UnusedPrivateTypeVar {
} = self;
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
}
fn fix_title(&self) -> Option<String> {
let UnusedPrivateTypeVar {
type_var_like_name,
type_var_like_kind,
} = self;
Some(format!(
"Remove unused private {type_var_like_kind} `{type_var_like_name}`"
))
}
}
/// ## What it does
@@ -197,7 +178,7 @@ pub(crate) fn unused_private_type_var(
let Some(source) = binding.source else {
continue;
};
let stmt @ Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
checker.semantic().statement(source)
else {
continue;
@@ -229,20 +210,13 @@ pub(crate) fn unused_private_type_var(
continue;
};
let mut diagnostic = Diagnostic::new(
diagnostics.push(Diagnostic::new(
UnusedPrivateTypeVar {
type_var_like_name: id.to_string(),
type_var_like_kind: type_var_like_kind.to_string(),
},
binding.range(),
);
if checker.settings.preview.is_enabled() {
let edit = fix::edits::delete_stmt(stmt, None, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::unsafe_edit(edit));
}
diagnostics.push(diagnostic);
));
}
}

View File

@@ -10,7 +10,6 @@ PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
|
= help: Remove unused private TypeVar `_T`
PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
@@ -20,7 +19,6 @@ PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
|
= help: Remove unused private TypeVarTuple `_Ts`
PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
@@ -31,7 +29,6 @@ PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P`
PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
@@ -41,7 +38,6 @@ PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
| ^^^ PYI018
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P2`
PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
@@ -52,4 +48,3 @@ PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
11 |
12 | # OK
|
= help: Remove unused private TypeVarTuple `_Ts2`

View File

@@ -10,7 +10,6 @@ PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
|
= help: Remove unused private TypeVar `_T`
PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
@@ -20,7 +19,6 @@ PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
|
= help: Remove unused private TypeVarTuple `_Ts`
PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
@@ -31,7 +29,6 @@ PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P`
PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
@@ -41,7 +38,6 @@ PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
| ^^^ PYI018
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P2`
PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
@@ -52,4 +48,3 @@ PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
11 |
12 | # OK
|
= help: Remove unused private TypeVarTuple `_Ts2`

View File

@@ -1,100 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI018.py:6:1: PYI018 [*] Private TypeVar `_T` is never used
|
4 | from typing_extensions import ParamSpec, TypeVarTuple
5 |
6 | _T = typing.TypeVar("_T")
| ^^ PYI018
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
|
= help: Remove unused private TypeVar `_T`
Unsafe fix
3 3 | from typing import TypeVar
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
5 5 |
6 |-_T = typing.TypeVar("_T")
7 6 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 7 | _P = ParamSpec("_P")
9 8 | _P2 = typing.ParamSpec("_P2")
PYI018.py:7:1: PYI018 [*] Private TypeVarTuple `_Ts` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
| ^^^ PYI018
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
|
= help: Remove unused private TypeVarTuple `_Ts`
Unsafe fix
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
5 5 |
6 6 | _T = typing.TypeVar("_T")
7 |-_Ts = typing_extensions.TypeVarTuple("_Ts")
8 7 | _P = ParamSpec("_P")
9 8 | _P2 = typing.ParamSpec("_P2")
10 9 | _Ts2 = TypeVarTuple("_Ts2")
PYI018.py:8:1: PYI018 [*] Private ParamSpec `_P` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
| ^^ PYI018
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P`
Unsafe fix
5 5 |
6 6 | _T = typing.TypeVar("_T")
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 |-_P = ParamSpec("_P")
9 8 | _P2 = typing.ParamSpec("_P2")
10 9 | _Ts2 = TypeVarTuple("_Ts2")
11 10 |
PYI018.py:9:1: PYI018 [*] Private ParamSpec `_P2` is never used
|
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
| ^^^ PYI018
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P2`
Unsafe fix
6 6 | _T = typing.TypeVar("_T")
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 8 | _P = ParamSpec("_P")
9 |-_P2 = typing.ParamSpec("_P2")
10 9 | _Ts2 = TypeVarTuple("_Ts2")
11 10 |
12 11 | # OK
PYI018.py:10:1: PYI018 [*] Private TypeVarTuple `_Ts2` is never used
|
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
| ^^^^ PYI018
11 |
12 | # OK
|
= help: Remove unused private TypeVarTuple `_Ts2`
Unsafe fix
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 8 | _P = ParamSpec("_P")
9 9 | _P2 = typing.ParamSpec("_P2")
10 |-_Ts2 = TypeVarTuple("_Ts2")
11 10 |
12 11 | # OK
13 12 | _UsedTypeVar = TypeVar("_UsedTypeVar")

View File

@@ -1,100 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI018.pyi:6:1: PYI018 [*] Private TypeVar `_T` is never used
|
4 | from typing_extensions import ParamSpec, TypeVarTuple
5 |
6 | _T = typing.TypeVar("_T")
| ^^ PYI018
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
|
= help: Remove unused private TypeVar `_T`
Unsafe fix
3 3 | from typing import TypeVar
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
5 5 |
6 |-_T = typing.TypeVar("_T")
7 6 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 7 | _P = ParamSpec("_P")
9 8 | _P2 = typing.ParamSpec("_P2")
PYI018.pyi:7:1: PYI018 [*] Private TypeVarTuple `_Ts` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
| ^^^ PYI018
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
|
= help: Remove unused private TypeVarTuple `_Ts`
Unsafe fix
4 4 | from typing_extensions import ParamSpec, TypeVarTuple
5 5 |
6 6 | _T = typing.TypeVar("_T")
7 |-_Ts = typing_extensions.TypeVarTuple("_Ts")
8 7 | _P = ParamSpec("_P")
9 8 | _P2 = typing.ParamSpec("_P2")
10 9 | _Ts2 = TypeVarTuple("_Ts2")
PYI018.pyi:8:1: PYI018 [*] Private ParamSpec `_P` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
| ^^ PYI018
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P`
Unsafe fix
5 5 |
6 6 | _T = typing.TypeVar("_T")
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 |-_P = ParamSpec("_P")
9 8 | _P2 = typing.ParamSpec("_P2")
10 9 | _Ts2 = TypeVarTuple("_Ts2")
11 10 |
PYI018.pyi:9:1: PYI018 [*] Private ParamSpec `_P2` is never used
|
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
| ^^^ PYI018
10 | _Ts2 = TypeVarTuple("_Ts2")
|
= help: Remove unused private ParamSpec `_P2`
Unsafe fix
6 6 | _T = typing.TypeVar("_T")
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 8 | _P = ParamSpec("_P")
9 |-_P2 = typing.ParamSpec("_P2")
10 9 | _Ts2 = TypeVarTuple("_Ts2")
11 10 |
12 11 | # OK
PYI018.pyi:10:1: PYI018 [*] Private TypeVarTuple `_Ts2` is never used
|
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
| ^^^^ PYI018
11 |
12 | # OK
|
= help: Remove unused private TypeVarTuple `_Ts2`
Unsafe fix
7 7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 8 | _P = ParamSpec("_P")
9 9 | _P2 = typing.ParamSpec("_P2")
10 |-_Ts2 = TypeVarTuple("_Ts2")
11 10 |
12 11 | # OK
13 12 | _UsedTypeVar = TypeVar("_UsedTypeVar")

View File

@@ -226,33 +226,6 @@ SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` inst
172 169 | if False:
173 170 | z = 1
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
170 | z = other
171 |
172 | / if False:
173 | | z = 1
174 | | else:
175 | | z = other
| |_____________^ SIM108
176 |
177 | if 1:
|
= help: Replace `if`-`else`-block with `z = 1 if False else other`
Unsafe fix
169 169 | else:
170 170 | z = other
171 171 |
172 |-if False:
173 |- z = 1
174 |-else:
175 |- z = other
172 |+z = 1 if False else other
176 173 |
177 174 | if 1:
178 175 | z = True
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
175 | z = other

View File

@@ -524,41 +524,4 @@ mod tests {
);
assert_messages!(snapshot, diagnostics);
}
#[test_case(
r"
from __future__ import annotations
TYPE_CHECKING = False
if TYPE_CHECKING:
from types import TracebackType
def foo(tb: TracebackType): ...
",
"github_issue_15681_regression_test"
)]
#[test_case(
r"
from __future__ import annotations
import pathlib # TC003
TYPE_CHECKING = False
if TYPE_CHECKING:
from types import TracebackType
def foo(tb: TracebackType) -> pathlib.Path: ...
",
"github_issue_15681_fix_test"
)]
fn contents_preview(contents: &str, snapshot: &str) {
let diagnostics = test_snippet(
contents,
&settings::LinterSettings {
preview: settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules())
},
);
assert_messages!(snapshot, diagnostics);
}
}

View File

@@ -1,26 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
<filename>:4:8: TC003 [*] Move standard library import `pathlib` into a type-checking block
|
2 | from __future__ import annotations
3 |
4 | import pathlib # TC003
| ^^^^^^^ TC003
5 |
6 | TYPE_CHECKING = False
|
= help: Move into type-checking block
Unsafe fix
1 1 |
2 2 | from __future__ import annotations
3 3 |
4 |-import pathlib # TC003
5 4 |
6 5 | TYPE_CHECKING = False
7 6 | if TYPE_CHECKING:
7 |+ import pathlib
8 8 | from types import TracebackType
9 9 |
10 10 | def foo(tb: TracebackType) -> pathlib.Path: ...

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---

View File

@@ -54,7 +54,7 @@ pub(crate) fn invalid_class_name(
name: &str,
ignore_names: &IgnoreNames,
) -> Option<Diagnostic> {
let stripped = name.trim_start_matches('_');
let stripped = name.strip_prefix('_').unwrap_or(name);
if !stripped.chars().next().is_some_and(char::is_uppercase) || stripped.contains('_') {
// Ignore any explicitly-allowed names.
if ignore_names.matches(name) {

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/pep8_naming/mod.rs
snapshot_kind: text
---
N801.py:1:7: N801 Class name `bad` should use CapWords convention
|
@@ -15,41 +16,23 @@ N801.py:5:7: N801 Class name `_bad` should use CapWords convention
6 | pass
|
N801.py:9:7: N801 Class name `__bad` should use CapWords convention
N801.py:9:7: N801 Class name `bad_class` should use CapWords convention
|
9 | class __bad:
| ^^^^^ N801
9 | class bad_class:
| ^^^^^^^^^ N801
10 | pass
|
N801.py:13:7: N801 Class name `bad_class` should use CapWords convention
N801.py:13:7: N801 Class name `Bad_Class` should use CapWords convention
|
13 | class bad_class:
13 | class Bad_Class:
| ^^^^^^^^^ N801
14 | pass
|
N801.py:17:7: N801 Class name `Bad_Class` should use CapWords convention
N801.py:17:7: N801 Class name `BAD_CLASS` should use CapWords convention
|
17 | class Bad_Class:
17 | class BAD_CLASS:
| ^^^^^^^^^ N801
18 | pass
|
N801.py:20:7: N801 Class name `Bad__Class` should use CapWords convention
|
18 | pass
19 |
20 | class Bad__Class:
| ^^^^^^^^^^ N801
21 | pass
|
N801.py:23:7: N801 Class name `BAD_CLASS` should use CapWords convention
|
21 | pass
22 |
23 | class BAD_CLASS:
| ^^^^^^^^^ N801
24 | pass
|

View File

@@ -44,7 +44,6 @@ mod tests {
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_3.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_4.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]

View File

@@ -1,3 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View File

@@ -57,7 +57,7 @@ mod native_literals;
mod open_alias;
mod os_error_alias;
mod outdated_version_block;
pub(crate) mod pep695;
mod pep695;
mod printf_string_formatting;
mod quoted_annotation;
mod redundant_open_modes;

View File

@@ -9,7 +9,7 @@ use ruff_python_ast::{
self as ast,
name::Name,
visitor::{self, Visitor},
Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam,
Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam,
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
};
use ruff_python_semantic::SemanticModel;
@@ -28,7 +28,7 @@ mod non_pep695_type_alias;
mod private_type_parameter;
#[derive(Debug)]
pub(crate) enum TypeVarRestriction<'a> {
enum TypeVarRestriction<'a> {
/// A type variable with a bound, e.g., `TypeVar("T", bound=int)`.
Bound(&'a Expr),
/// A type variable with constraints, e.g., `TypeVar("T", int, str)`.
@@ -39,25 +39,25 @@ pub(crate) enum TypeVarRestriction<'a> {
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum TypeParamKind {
enum TypeParamKind {
TypeVar,
TypeVarTuple,
ParamSpec,
}
#[derive(Debug)]
pub(crate) struct TypeVar<'a> {
pub(crate) name: &'a str,
pub(crate) restriction: Option<TypeVarRestriction<'a>>,
pub(crate) kind: TypeParamKind,
pub(crate) default: Option<&'a Expr>,
struct TypeVar<'a> {
name: &'a str,
restriction: Option<TypeVarRestriction<'a>>,
kind: TypeParamKind,
default: Option<&'a Expr>,
}
/// Wrapper for formatting a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T,
/// *Ts, **P]`). See [`DisplayTypeVar`] for further details.
pub(crate) struct DisplayTypeVars<'a> {
pub(crate) type_vars: &'a [TypeVar<'a>],
pub(crate) source: &'a str,
struct DisplayTypeVars<'a> {
type_vars: &'a [TypeVar<'a>],
source: &'a str,
}
impl Display for DisplayTypeVars<'_> {
@@ -81,7 +81,7 @@ impl Display for DisplayTypeVars<'_> {
/// Used for displaying `type_var`. `source` is the whole file, which will be sliced to recover the
/// `TypeVarRestriction` values for generic bounds and constraints.
pub(crate) struct DisplayTypeVar<'a> {
struct DisplayTypeVar<'a> {
type_var: &'a TypeVar<'a>,
source: &'a str,
}
@@ -192,34 +192,6 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam {
}
}
impl<'a> From<&'a TypeParam> for TypeVar<'a> {
fn from(param: &'a TypeParam) -> Self {
let (kind, restriction) = match param {
TypeParam::TypeVarTuple(_) => (TypeParamKind::TypeVarTuple, None),
TypeParam::ParamSpec(_) => (TypeParamKind::ParamSpec, None),
TypeParam::TypeVar(param) => {
let restriction = match param.bound.as_deref() {
None => None,
Some(Expr::Tuple(constraints)) => Some(TypeVarRestriction::Constraint(
constraints.elts.iter().collect::<Vec<_>>(),
)),
Some(bound) => Some(TypeVarRestriction::Bound(bound)),
};
(TypeParamKind::TypeVar, restriction)
}
};
Self {
name: param.name(),
kind,
restriction,
default: param.default(),
}
}
}
struct TypeVarReferenceVisitor<'a> {
vars: Vec<TypeVar<'a>>,
semantic: &'a SemanticModel<'a>,
@@ -270,7 +242,7 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> {
}
}
pub(crate) fn expr_name_to_type_var<'a>(
fn expr_name_to_type_var<'a>(
semantic: &'a SemanticModel,
name: &'a ExprName,
) -> Option<TypeVar<'a>> {
@@ -377,18 +349,3 @@ fn check_type_vars(vars: Vec<TypeVar<'_>>) -> Option<Vec<TypeVar<'_>>> {
== vars.len())
.then_some(vars)
}
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if
/// any), along with its index in the class's bases tuple.
pub(crate) fn find_generic<'a>(
class_bases: &'a Arguments,
semantic: &SemanticModel,
) -> Option<(usize, &'a ExprSubscript)> {
class_bases.args.iter().enumerate().find_map(|(idx, expr)| {
expr.as_subscript_expr().and_then(|sub_expr| {
semantic
.match_typing_expr(&sub_expr.value, "Generic")
.then_some((idx, sub_expr))
})
})
}

View File

@@ -1,16 +1,15 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{ExprSubscript, StmtClassDef};
use ruff_python_ast::{Arguments, ExprSubscript, StmtClassDef};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
use crate::settings::types::PythonVersion;
use super::{
check_type_vars, find_generic, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor,
};
use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor};
/// ## What it does
///
@@ -212,3 +211,18 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl
checker.diagnostics.push(diagnostic);
}
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if
/// any), along with its index in the class's bases tuple.
fn find_generic<'a>(
class_bases: &'a Arguments,
semantic: &SemanticModel,
) -> Option<(usize, &'a ExprSubscript)> {
class_bases.args.iter().enumerate().find_map(|(idx, expr)| {
expr.as_subscript_expr().and_then(|sub_expr| {
semantic
.match_typing_expr(&sub_expr.value, "Generic")
.then_some((idx, sub_expr))
})
})
}

View File

@@ -435,7 +435,6 @@ mod tests {
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -1,247 +0,0 @@
use rustc_hash::FxHashSet;
use std::iter;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{
Arguments, Expr, ExprStarred, ExprSubscript, ExprTuple, StmtClassDef, TypeParams,
};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
use crate::rules::pyupgrade::rules::pep695::{
expr_name_to_type_var, find_generic, DisplayTypeVars, TypeParamKind, TypeVar,
};
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks for classes that have [PEP 695] [type parameter lists]
/// while also inheriting from `typing.Generic` or `typing_extensions.Generic`.
///
/// ## Why is this bad?
/// Such classes cause errors at runtime:
///
/// ```python
/// from typing import Generic, TypeVar
///
/// U = TypeVar("U")
///
/// # TypeError: Cannot inherit from Generic[...] multiple times.
/// class C[T](Generic[U]): ...
/// ```
///
/// ## Example
///
/// ```python
/// from typing import Generic, ParamSpec, TypeVar, TypeVarTuple
///
/// U = TypeVar("U")
/// P = ParamSpec("P")
/// Ts = TypeVarTuple("Ts")
///
///
/// class C[T](Generic[U, P, *Ts]): ...
/// ```
///
/// Use instead:
///
/// ```python
/// class C[T, U, **P, *Ts]: ...
/// ```
///
/// ## Fix safety
/// As the fix changes runtime behaviour, it is always marked as unsafe.
/// Additionally, comments within the fix range will not be preserved.
///
/// ## References
/// - [Python documentation: User-defined generic types](https://docs.python.org/3/library/typing.html#user-defined-generic-types)
/// - [Python documentation: type parameter lists](https://docs.python.org/3/reference/compound_stmts.html#type-params)
/// - [PEP 695 - Type Parameter Syntax](https://peps.python.org/pep-0695/)
///
/// [PEP 695]: https://peps.python.org/pep-0695/
/// [type parameter lists]: https://docs.python.org/3/reference/compound_stmts.html#type-params
#[derive(ViolationMetadata)]
pub(crate) struct ClassWithMixedTypeVars;
impl Violation for ClassWithMixedTypeVars {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Class with type parameter list inherits from `Generic`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Remove `Generic` base class".to_string())
}
}
/// RUF053
pub(crate) fn class_with_mixed_type_vars(checker: &mut Checker, class_def: &StmtClassDef) {
if checker.settings.target_version < PythonVersion::Py312 {
return;
}
let StmtClassDef {
type_params,
arguments,
..
} = class_def;
let Some(type_params) = type_params else {
return;
};
let Some(arguments) = arguments else {
return;
};
let Some((generic_base, old_style_type_vars)) =
typing_generic_base_and_arguments(arguments, checker.semantic())
else {
return;
};
let mut diagnostic = Diagnostic::new(ClassWithMixedTypeVars, generic_base.range);
diagnostic.try_set_optional_fix(|| {
convert_type_vars(
generic_base,
old_style_type_vars,
type_params,
arguments,
checker,
)
});
checker.diagnostics.push(diagnostic);
}
fn typing_generic_base_and_arguments<'a>(
class_arguments: &'a Arguments,
semantic: &SemanticModel,
) -> Option<(&'a ExprSubscript, &'a Expr)> {
let (_, base @ ExprSubscript { slice, .. }) = find_generic(class_arguments, semantic)?;
Some((base, slice.as_ref()))
}
fn convert_type_vars(
generic_base: &ExprSubscript,
old_style_type_vars: &Expr,
type_params: &TypeParams,
class_arguments: &Arguments,
checker: &Checker,
) -> anyhow::Result<Option<Fix>> {
let mut type_vars: Vec<_> = type_params.type_params.iter().map(TypeVar::from).collect();
let semantic = checker.semantic();
let converted_type_vars = match old_style_type_vars {
Expr::Tuple(ExprTuple { elts, .. }) => {
generic_arguments_to_type_vars(elts.iter(), type_params, semantic)
}
expr @ (Expr::Subscript(_) | Expr::Name(_)) => {
generic_arguments_to_type_vars(iter::once(expr), type_params, semantic)
}
_ => None,
};
let Some(converted_type_vars) = converted_type_vars else {
return Ok(None);
};
type_vars.extend(converted_type_vars);
let source = checker.source();
let new_type_params = DisplayTypeVars {
type_vars: &type_vars,
source,
};
let remove_generic_base =
remove_argument(generic_base, class_arguments, Parentheses::Remove, source)?;
let replace_type_params =
Edit::range_replacement(new_type_params.to_string(), type_params.range);
Ok(Some(Fix::unsafe_edits(
remove_generic_base,
[replace_type_params],
)))
}
/// Returns the type variables `exprs` represent.
///
/// If at least one of them cannot be converted to [`TypeVar`],
/// `None` is returned.
fn generic_arguments_to_type_vars<'a>(
exprs: impl Iterator<Item = &'a Expr>,
existing_type_params: &TypeParams,
semantic: &'a SemanticModel,
) -> Option<Vec<TypeVar<'a>>> {
let mut type_vars = vec![];
let mut encountered: FxHashSet<&str> = existing_type_params
.iter()
.map(|tp| tp.name().as_str())
.collect();
for expr in exprs {
let (name, unpacked) = match expr {
Expr::Name(name) => (name, false),
Expr::Starred(ExprStarred { value, .. }) => (value.as_name_expr()?, true),
Expr::Subscript(ExprSubscript { value, slice, .. }) => {
if !semantic.match_typing_expr(value, "Unpack") {
return None;
}
(slice.as_name_expr()?, true)
}
_ => return None,
};
if !encountered.insert(name.id.as_str()) {
continue;
}
let type_var = expr_name_to_type_var(semantic, name)?;
if !type_var_is_valid(&type_var, unpacked) {
return None;
}
// TODO: Type parameter defaults
if type_var.default.is_some() {
return None;
}
type_vars.push(type_var);
}
Some(type_vars)
}
/// Returns true in the following cases:
///
/// * If `type_var` is a `TypeVar`:
/// * It must not be unpacked
/// * If `type_var` is a `TypeVarTuple`:
/// * It must be unpacked
/// * It must not have any restrictions
/// * If `type_var` is a `ParamSpec`:
/// * It must not be unpacked
/// * It must not have any restrictions
fn type_var_is_valid(type_var: &TypeVar, unpacked: bool) -> bool {
let is_type_var_tuple = matches!(&type_var.kind, TypeParamKind::TypeVarTuple);
if is_type_var_tuple && !unpacked || !is_type_var_tuple && unpacked {
return false;
}
if !matches!(&type_var.kind, TypeParamKind::TypeVar) && type_var.restriction.is_some() {
return false;
}
true
}

View File

@@ -2,7 +2,6 @@ pub(crate) use ambiguous_unicode_character::*;
pub(crate) use assert_with_print_message::*;
pub(crate) use assignment_in_assert::*;
pub(crate) use asyncio_dangling_task::*;
pub(crate) use class_with_mixed_type_vars::*;
pub(crate) use collection_literal_concatenation::*;
pub(crate) use dataclass_enum::*;
pub(crate) use decimal_from_float_literal::*;
@@ -56,7 +55,6 @@ mod ambiguous_unicode_character;
mod assert_with_print_message;
mod assignment_in_assert;
mod asyncio_dangling_task;
mod class_with_mixed_type_vars;
mod collection_literal_concatenation;
mod confusables;
mod dataclass_enum;

View File

@@ -24,12 +24,7 @@ use crate::rules::ruff::rules::helpers::{
/// `typing.ClassVar`. When mutability is not required, values should be
/// immutable types, like `tuple` or `frozenset`.
///
/// For mutable variables, prefer to initialize them in `__init__`.
///
/// ## Examples
///
/// Using `ClassVar` and imutable types:
///
/// ```python
/// class A:
/// mutable_default: list[int] = []
@@ -37,7 +32,6 @@ use crate::rules::ruff::rules::helpers::{
/// ```
///
/// Use instead:
///
/// ```python
/// from typing import ClassVar
///
@@ -46,27 +40,6 @@ use crate::rules::ruff::rules::helpers::{
/// mutable_default: ClassVar[list[int]] = []
/// immutable_default: tuple[int, ...] = ()
/// ```
///
/// Using instance variables instead of class variables:
///
/// ```python
/// class A:
/// instance_dict: dict[str, str] = {"key": "value"}
/// ```
///
/// Use instead:
///
/// ```python
/// class A:
/// instance_dict: ClassVar[dict[str, str]]
///
/// def __init__(self) -> None:
/// self.instance_dict: dict[str, str] = {"key": "value"}
/// ```
///
/// In cases where memory efficiency is a priority, `MappingProxyType`
/// can be used to create immutable dictionaries that are shared between
/// instances.
#[derive(ViolationMetadata)]
pub(crate) struct MutableClassDefault;

View File

@@ -1,475 +0,0 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF053.py:23:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
21 | ### Errors
22 |
23 | class C[T](Generic[_A]): ...
| ^^^^^^^^^^^ RUF053
24 | class C[T](Generic[_B], str): ...
25 | class C[T](int, Generic[_C]): ...
|
= help: Remove `Generic` base class
Unsafe fix
20 20 |
21 21 | ### Errors
22 22 |
23 |-class C[T](Generic[_A]): ...
23 |+class C[T, _A]: ...
24 24 | class C[T](Generic[_B], str): ...
25 25 | class C[T](int, Generic[_C]): ...
26 26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
RUF053.py:24:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
23 | class C[T](Generic[_A]): ...
24 | class C[T](Generic[_B], str): ...
| ^^^^^^^^^^^ RUF053
25 | class C[T](int, Generic[_C]): ...
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
Unsafe fix
21 21 | ### Errors
22 22 |
23 23 | class C[T](Generic[_A]): ...
24 |-class C[T](Generic[_B], str): ...
24 |+class C[T, _B: int](str): ...
25 25 | class C[T](int, Generic[_C]): ...
26 26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
27 27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
RUF053.py:25:17: RUF053 [*] Class with type parameter list inherits from `Generic`
|
23 | class C[T](Generic[_A]): ...
24 | class C[T](Generic[_B], str): ...
25 | class C[T](int, Generic[_C]): ...
| ^^^^^^^^^^^ RUF053
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
Unsafe fix
22 22 |
23 23 | class C[T](Generic[_A]): ...
24 24 | class C[T](Generic[_B], str): ...
25 |-class C[T](int, Generic[_C]): ...
25 |+class C[T, _C: (str, bytes)](int): ...
26 26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
27 27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
28 28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
RUF053.py:26:19: RUF053 Class with type parameter list inherits from `Generic`
|
24 | class C[T](Generic[_B], str): ...
25 | class C[T](int, Generic[_C]): ...
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^ RUF053
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
RUF053.py:27:12: RUF053 Class with type parameter list inherits from `Generic`
|
25 | class C[T](int, Generic[_C]): ...
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^ RUF053
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
RUF053.py:28:22: RUF053 Class with type parameter list inherits from `Generic`
|
26 | class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^ RUF053
29 |
30 | class C[*Ts](Generic[*_As]): ...
|
= help: Remove `Generic` base class
RUF053.py:30:14: RUF053 [*] Class with type parameter list inherits from `Generic`
|
28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
29 |
30 | class C[*Ts](Generic[*_As]): ...
| ^^^^^^^^^^^^^ RUF053
31 | class C[*Ts](Generic[Unpack[_As]]): ...
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
|
= help: Remove `Generic` base class
Unsafe fix
27 27 | class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
28 28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
29 29 |
30 |-class C[*Ts](Generic[*_As]): ...
30 |+class C[*Ts, *_As]: ...
31 31 | class C[*Ts](Generic[Unpack[_As]]): ...
32 32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
33 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
RUF053.py:31:14: RUF053 [*] Class with type parameter list inherits from `Generic`
|
30 | class C[*Ts](Generic[*_As]): ...
31 | class C[*Ts](Generic[Unpack[_As]]): ...
| ^^^^^^^^^^^^^^^^^^^^ RUF053
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
Unsafe fix
28 28 | class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
29 29 |
30 30 | class C[*Ts](Generic[*_As]): ...
31 |-class C[*Ts](Generic[Unpack[_As]]): ...
31 |+class C[*Ts, *_As]: ...
32 32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
33 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
34 34 |
RUF053.py:32:14: RUF053 Class with type parameter list inherits from `Generic`
|
30 | class C[*Ts](Generic[*_As]): ...
31 | class C[*Ts](Generic[Unpack[_As]]): ...
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
| ^^^^^^^^^^^^^^^^^^^^ RUF053
33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
RUF053.py:33:44: RUF053 Class with type parameter list inherits from `Generic`
|
31 | class C[*Ts](Generic[Unpack[_As]]): ...
32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:36:14: RUF053 [*] Class with type parameter list inherits from `Generic`
|
36 | class C[**P](Generic[_P1]): ...
| ^^^^^^^^^^^^ RUF053
37 | class C[**P](Generic[_P2]): ...
38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
Unsafe fix
33 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
34 34 |
35 35 |
36 |-class C[**P](Generic[_P1]): ...
36 |+class C[**P, **_P1]: ...
37 37 | class C[**P](Generic[_P2]): ...
38 38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
39 39 |
RUF053.py:37:14: RUF053 Class with type parameter list inherits from `Generic`
|
36 | class C[**P](Generic[_P1]): ...
37 | class C[**P](Generic[_P2]): ...
| ^^^^^^^^^^^^ RUF053
38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
RUF053.py:38:14: RUF053 Class with type parameter list inherits from `Generic`
|
36 | class C[**P](Generic[_P1]): ...
37 | class C[**P](Generic[_P2]): ...
38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:41:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
41 | class C[T](Generic[T, _A]): ...
| ^^^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
Unsafe fix
38 38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
39 39 |
40 40 |
41 |-class C[T](Generic[T, _A]): ...
41 |+class C[T, _A]: ...
42 42 |
43 43 |
44 44 | # See `is_existing_param_of_same_class`.
RUF053.py:47:35: RUF053 Class with type parameter list inherits from `Generic`
|
45 | # `expr_name_to_type_var` doesn't handle named expressions,
46 | # only simple assignments, so there is no fix.
47 | class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ...
| ^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:51:16: RUF053 [*] Class with type parameter list inherits from `Generic`
|
50 | class C(Generic[_B]):
51 | class D[T](Generic[_B, T]): ...
| ^^^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
Unsafe fix
48 48 |
49 49 |
50 50 | class C(Generic[_B]):
51 |- class D[T](Generic[_B, T]): ...
51 |+ class D[T, _B: int]: ...
52 52 |
53 53 |
54 54 | class C[T]:
RUF053.py:55:16: RUF053 Class with type parameter list inherits from `Generic`
|
54 | class C[T]:
55 | class D[U](Generic[T, U]): ...
| ^^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:60:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
58 | # In a single run, only the first is reported.
59 | # Others will be reported/fixed in following iterations.
60 | class C[T](Generic[_C], Generic[_D]): ...
| ^^^^^^^^^^^ RUF053
61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
|
= help: Remove `Generic` base class
Unsafe fix
57 57 |
58 58 | # In a single run, only the first is reported.
59 59 | # Others will be reported/fixed in following iterations.
60 |-class C[T](Generic[_C], Generic[_D]): ...
60 |+class C[T, _C: (str, bytes)](Generic[_D]): ...
61 61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
62 62 |
63 63 |
RUF053.py:61:30: RUF053 Class with type parameter list inherits from `Generic`
|
59 | # Others will be reported/fixed in following iterations.
60 | class C[T](Generic[_C], Generic[_D]): ...
61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:66:3: RUF053 Class with type parameter list inherits from `Generic`
|
64 | class C[
65 | T # Comment
66 | ](Generic[_E]): ... # TODO: Type parameter defaults
| ^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:69:12: RUF053 Class with type parameter list inherits from `Generic`
|
69 | class C[T](Generic[Generic[_F]]): ...
| ^^^^^^^^^^^^^^^^^^^^ RUF053
70 | class C[T](Generic[Unpack[_A]]): ...
71 | class C[T](Generic[Unpack[_P1]]): ...
|
= help: Remove `Generic` base class
RUF053.py:70:12: RUF053 Class with type parameter list inherits from `Generic`
|
69 | class C[T](Generic[Generic[_F]]): ...
70 | class C[T](Generic[Unpack[_A]]): ...
| ^^^^^^^^^^^^^^^^^^^ RUF053
71 | class C[T](Generic[Unpack[_P1]]): ...
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
|
= help: Remove `Generic` base class
RUF053.py:71:12: RUF053 Class with type parameter list inherits from `Generic`
|
69 | class C[T](Generic[Generic[_F]]): ...
70 | class C[T](Generic[Unpack[_A]]): ...
71 | class C[T](Generic[Unpack[_P1]]): ...
| ^^^^^^^^^^^^^^^^^^^^ RUF053
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
73 | class C[T](Generic[Unpack[*_As]]): ...
|
= help: Remove `Generic` base class
RUF053.py:72:12: RUF053 Class with type parameter list inherits from `Generic`
|
70 | class C[T](Generic[Unpack[_A]]): ...
71 | class C[T](Generic[Unpack[_P1]]): ...
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
73 | class C[T](Generic[Unpack[*_As]]): ...
74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
|
= help: Remove `Generic` base class
RUF053.py:73:12: RUF053 Class with type parameter list inherits from `Generic`
|
71 | class C[T](Generic[Unpack[_P1]]): ...
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
73 | class C[T](Generic[Unpack[*_As]]): ...
| ^^^^^^^^^^^^^^^^^^^^^ RUF053
74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
|
= help: Remove `Generic` base class
RUF053.py:74:12: RUF053 Class with type parameter list inherits from `Generic`
|
72 | class C[T](Generic[Unpack[Unpack[_P2]]]): ...
73 | class C[T](Generic[Unpack[*_As]]): ...
74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:77:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
77 | class C[T](Generic[_A, _A]): ...
| ^^^^^^^^^^^^^^^ RUF053
78 | class C[T](Generic[_A, Unpack[_As]]): ...
79 | class C[T](Generic[*_As, _A]): ...
|
= help: Remove `Generic` base class
Unsafe fix
74 74 | class C[T](Generic[Unpack[_As, _Bs]]): ...
75 75 |
76 76 |
77 |-class C[T](Generic[_A, _A]): ...
77 |+class C[T, _A]: ...
78 78 | class C[T](Generic[_A, Unpack[_As]]): ...
79 79 | class C[T](Generic[*_As, _A]): ...
80 80 |
RUF053.py:78:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
77 | class C[T](Generic[_A, _A]): ...
78 | class C[T](Generic[_A, Unpack[_As]]): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
79 | class C[T](Generic[*_As, _A]): ...
|
= help: Remove `Generic` base class
Unsafe fix
75 75 |
76 76 |
77 77 | class C[T](Generic[_A, _A]): ...
78 |-class C[T](Generic[_A, Unpack[_As]]): ...
78 |+class C[T, _A, *_As]: ...
79 79 | class C[T](Generic[*_As, _A]): ...
80 80 |
81 81 |
RUF053.py:79:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
77 | class C[T](Generic[_A, _A]): ...
78 | class C[T](Generic[_A, Unpack[_As]]): ...
79 | class C[T](Generic[*_As, _A]): ...
| ^^^^^^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
Unsafe fix
76 76 |
77 77 | class C[T](Generic[_A, _A]): ...
78 78 | class C[T](Generic[_A, Unpack[_As]]): ...
79 |-class C[T](Generic[*_As, _A]): ...
79 |+class C[T, *_As, _A]: ...
80 80 |
81 81 |
82 82 | from somewhere import APublicTypeVar
RUF053.py:83:12: RUF053 Class with type parameter list inherits from `Generic`
|
82 | from somewhere import APublicTypeVar
83 | class C[T](Generic[APublicTypeVar]): ...
| ^^^^^^^^^^^^^^^^^^^^^^^ RUF053
84 | class C[T](Generic[APublicTypeVar, _A]): ...
|
= help: Remove `Generic` base class
RUF053.py:84:12: RUF053 Class with type parameter list inherits from `Generic`
|
82 | from somewhere import APublicTypeVar
83 | class C[T](Generic[APublicTypeVar]): ...
84 | class C[T](Generic[APublicTypeVar, _A]): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
RUF053.py:91:12: RUF053 [*] Class with type parameter list inherits from `Generic`
|
89 | # as named expressions are forbidden within type parameter lists.
90 | # See also the `_Z` example above.
91 | class C[T](Generic[_G]): ... # Should be moved down below eventually
| ^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
Unsafe fix
88 88 | # The latter cannot be used as a PEP 695 constraint,
89 89 | # as named expressions are forbidden within type parameter lists.
90 90 | # See also the `_Z` example above.
91 |-class C[T](Generic[_G]): ... # Should be moved down below eventually
91 |+class C[T, _G: (str, a := int)]: ... # Should be moved down below eventually
92 92 |
93 93 |
94 94 | # Single-element constraints should not be converted to a bound.
RUF053.py:95:20: RUF053 [*] Class with type parameter list inherits from `Generic`
|
94 | # Single-element constraints should not be converted to a bound.
95 | class C[T: (str,)](Generic[_A]): ...
| ^^^^^^^^^^^ RUF053
96 | class C[T: [a]](Generic[_A]): ...
|
= help: Remove `Generic` base class
Unsafe fix
92 92 |
93 93 |
94 94 | # Single-element constraints should not be converted to a bound.
95 |-class C[T: (str,)](Generic[_A]): ...
95 |+class C[T: (str), _A]: ...
96 96 | class C[T: [a]](Generic[_A]): ...
97 97 |
98 98 |
RUF053.py:96:17: RUF053 [*] Class with type parameter list inherits from `Generic`
|
94 | # Single-element constraints should not be converted to a bound.
95 | class C[T: (str,)](Generic[_A]): ...
96 | class C[T: [a]](Generic[_A]): ...
| ^^^^^^^^^^^ RUF053
|
= help: Remove `Generic` base class
Unsafe fix
93 93 |
94 94 | # Single-element constraints should not be converted to a bound.
95 95 | class C[T: (str,)](Generic[_A]): ...
96 |-class C[T: [a]](Generic[_A]): ...
96 |+class C[T: [a], _A]: ...
97 97 |
98 98 |
99 99 | # Existing bounds should not be deparenthesized.

View File

@@ -69,7 +69,7 @@ pub enum TomlSourceType {
Unrecognized,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PySourceType {
/// The source is a Python file (`.py`).

View File

@@ -374,15 +374,12 @@ impl fmt::Display for DocstringCode {
}
#[derive(Copy, Clone, Default, Eq, PartialEq, CacheKey)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(untagged, rename_all = "lowercase")
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DocstringCodeLineWidth {
/// Wrap docstring code examples at a fixed line width.
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema::fixed"))]
Fixed(LineWidth),
/// Respect the line length limit setting for the surrounding Python code.
@@ -391,45 +388,27 @@ pub enum DocstringCodeLineWidth {
feature = "serde",
serde(deserialize_with = "deserialize_docstring_code_line_width_dynamic")
)]
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema::dynamic"))]
#[cfg_attr(feature = "schemars", schemars(with = "DynamicSchema"))]
Dynamic,
}
/// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`.
#[cfg(feature = "schemars")]
mod schema {
use ruff_formatter::LineWidth;
use schemars::gen::SchemaGenerator;
use schemars::schema::{Metadata, Schema, SubschemaValidation};
struct DynamicSchema;
/// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`.
pub(super) fn dynamic(_: &mut SchemaGenerator) -> Schema {
Schema::Object(schemars::schema::SchemaObject {
const_value: Some("dynamic".to_string().into()),
..Default::default()
})
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for DynamicSchema {
fn schema_name() -> String {
"Dynamic".to_string()
}
// We use a manual schema for `fixed` even thought it isn't strictly necessary according to the
// JSON schema specification to work around a bug in Even Better TOML with `allOf`.
// https://github.com/astral-sh/ruff/issues/15978#issuecomment-2639547101
//
// The only difference to the automatically derived schema is that we use `oneOf` instead of
// `allOf`. There's no semantic difference between `allOf` and `oneOf` for single element lists.
pub(super) fn fixed(gen: &mut SchemaGenerator) -> Schema {
let schema = gen.subschema_for::<LineWidth>();
Schema::Object(schemars::schema::SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(
"Wrap docstring code examples at a fixed line width.".to_string(),
),
..Metadata::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![schema]),
..SubschemaValidation::default()
})),
fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
const_value: Some("dynamic".to_string().into()),
..Default::default()
})
}
.into()
}
}

View File

@@ -8,37 +8,33 @@ use crate::SemanticModel;
/// import sys
///
/// sys.path.append("../")
/// sys.path += ["../"]
/// ```
pub fn is_sys_path_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool {
match stmt {
Stmt::Expr(ast::StmtExpr { value, range: _ }) => match value.as_ref() {
Expr::Call(ast::ExprCall { func, .. }) => semantic
.resolve_qualified_name(func.as_ref())
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
[
"sys",
"path",
"append"
| "insert"
| "extend"
| "remove"
| "pop"
| "clear"
| "reverse"
| "sort"
]
)
}),
_ => false,
},
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => semantic
.resolve_qualified_name(map_subscript(target))
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["sys", "path"])),
_ => false,
}
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return false;
};
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
return false;
};
semantic
.resolve_qualified_name(func.as_ref())
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
[
"sys",
"path",
"append"
| "insert"
| "extend"
| "remove"
| "pop"
| "clear"
| "reverse"
| "sort"
]
)
})
}
/// Returns `true` if a [`Stmt`] is an `os.environ` modification, as in:

View File

@@ -382,22 +382,6 @@ pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
let ast::StmtIf { test, .. } = stmt;
if semantic.use_new_type_checking_block_detection_semantics() {
return match test.as_ref() {
// As long as the symbol's name is "TYPE_CHECKING" we will treat it like `typing.TYPE_CHECKING`
// for this specific check even if it's defined somewhere else, like the current module.
// Ex) `if TYPE_CHECKING:`
Expr::Name(ast::ExprName { id, .. }) => {
id == "TYPE_CHECKING"
// Ex) `if TC:` with `from typing import TYPE_CHECKING as TC`
|| semantic.match_typing_expr(test, "TYPE_CHECKING")
}
// Ex) `if typing.TYPE_CHECKING:`
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING",
_ => false,
};
}
// Ex) `if False:`
if is_const_false(test) {
return true;

View File

@@ -2014,18 +2014,6 @@ impl<'a> SemanticModel<'a> {
.intersects(SemanticModelFlags::DEFERRED_CLASS_BASE)
}
/// Return `true` if we should use the new semantics to recognize
/// type checking blocks. Previously we only recognized type checking
/// blocks if `TYPE_CHECKING` was imported from a typing module.
///
/// With this feature flag enabled we recognize any symbol named
/// `TYPE_CHECKING`, regardless of where it comes from to mirror
/// what mypy and pyright do.
pub const fn use_new_type_checking_block_detection_semantics(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION)
}
/// Return an iterator over all bindings shadowed by the given [`BindingId`], within the
/// containing scope, and across scopes.
pub fn shadowed_bindings(
@@ -2557,14 +2545,6 @@ bitflags! {
/// [#13824]: https://github.com/astral-sh/ruff/issues/13824
const NO_TYPE_CHECK = 1 << 30;
/// The model special-cases any symbol named `TYPE_CHECKING`.
///
/// Previously we only recognized `TYPE_CHECKING` if it was part of
/// one of the configured `typing` modules. This flag exists to
/// test out the semantic change only in preview. This flag will go
/// away once this change has been stabilized.
const NEW_TYPE_CHECKING_BLOCK_DETECTION = 1 << 31;
/// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.9.5"
version = "0.9.4"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -9,22 +9,22 @@ While `ruff server` supports the same feature set as [`ruff-lsp`](https://github
settings are supported by `ruff server`. As such, this migration guide is primarily targeted at editors that lack
explicit documentation for `ruff server` settings, such as Helix or Neovim.
Refer to the [setup guide](setup.md) for instructions on how to configure your editor to use `ruff server`.
## Unsupported Settings
Several `ruff-lsp` settings are not supported by `ruff server`. These are, as follows:
- `lint.run`: This setting is no longer relevant for the native language server, which runs on every
keystroke by default
- `lint.args`, `format.args`: These settings have been replaced by more granular settings in `ruff server` like [`lint.select`](settings.md#select), [`format.preview`](settings.md#format_preview),
etc. along with the ability to provide a default configuration file using
[`configuration`](settings.md#configuration)
- [`path`](settings.md#path), [`interpreter`](settings.md#interpreter): These settings are no longer
accepted by the language server but are still used by the VS Code extension. Refer to their
respective documentation for more information on how it's being used by the extension.
- `format.args`
- `ignoreStandardLibrary`
- `showNotifications`
- `interpreter`
- `lint.args`
- `lint.run`
- `path`
!!! note
Some of these settings, like `interpreter` and `path`, are still accepted by the VS Code
extension. `path`, in particular, can be used to specify a dedicated binary to use when
initializing `ruff server`. But the language server itself will no longer accept such settings.
## New Settings

View File

@@ -882,12 +882,6 @@ Whether to enable the Ruff extension. Modifying this setting requires restarting
### `format.args`
!!! warning "Deprecated"
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
deprecated in favor of the native language server. Refer to the [migration
guide](migration.md) for more information.
_**This setting is not used by the native language server.**_
Additional arguments to pass to the Ruff formatter.
@@ -906,12 +900,6 @@ Additional arguments to pass to the Ruff formatter.
### `ignoreStandardLibrary`
!!! warning "Deprecated"
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
deprecated in favor of the native language server. Refer to the [migration
guide](migration.md) for more information.
_**This setting is not used by the native language server.**_
Whether to ignore files that are inferred to be part of the Python standard library.
@@ -972,12 +960,6 @@ This setting depends on the [`ruff.nativeServer`](#nativeserver) setting:
### `lint.args`
!!! warning "Deprecated"
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
deprecated in favor of the native language server. Refer to the [migration
guide](migration.md) for more information.
_**This setting is not used by the native language server.**_
Additional arguments to pass to the Ruff linter.
@@ -996,12 +978,6 @@ Additional arguments to pass to the Ruff linter.
### `lint.run`
!!! warning "Deprecated"
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
deprecated in favor of the native language server. Refer to the [migration
guide](migration.md) for more information.
_**This setting is not used by the native language server.**_
Run Ruff on every keystroke (`onType`) or on save (`onSave`).
@@ -1020,14 +996,6 @@ Run Ruff on every keystroke (`onType`) or on save (`onSave`).
### `nativeServer`
!!! warning "Deprecated"
This setting has been deprecated with the deprecation of
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp). It was mainly used to provide a way to
switch between the native language server and
[`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) during the transition period. Refer to the
[migration guide](migration.md) for more information.
Whether to use the native language server, [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) or
automatically decide between the two based on the Ruff version and extension settings.
@@ -1078,12 +1046,6 @@ The first executable in the list which is exists is used. This setting takes pre
### `showNotifications`
!!! warning "Deprecated"
This setting is only used by [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) which is
deprecated in favor of the native language server. Refer to the [migration
guide](migration.md) for more information.
Setting to control when a notification is shown.
**Default value**: `"off"`

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.9.5-alpine
name: ghcr.io/astral-sh/ruff:0.9.4-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.5
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.5
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.5
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff

View File

@@ -365,7 +365,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.5
rev: v0.9.4
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.9.5"
version = "0.9.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

13
ruff.schema.json generated
View File

@@ -879,7 +879,7 @@
"anyOf": [
{
"description": "Wrap docstring code examples at a fixed line width.",
"oneOf": [
"allOf": [
{
"$ref": "#/definitions/LineWidth"
}
@@ -887,10 +887,18 @@
},
{
"description": "Respect the line length limit setting for the surrounding Python code.",
"const": "dynamic"
"allOf": [
{
"$ref": "#/definitions/Dynamic"
}
]
}
]
},
"Dynamic": {
"type": "string",
"const": "dynamic"
},
"Flake8AnnotationsOptions": {
"description": "Options for the `flake8-annotations` plugin.",
"type": "object",
@@ -3947,7 +3955,6 @@
"RUF05",
"RUF051",
"RUF052",
"RUF053",
"RUF055",
"RUF056",
"RUF057",

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.9.5"
version = "0.9.4"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]