Compare commits

..

14 Commits

Author SHA1 Message Date
Charlie Marsh
634765c303 [ty] Emit a diagnostic for subclassing with order=True 2026-01-06 11:10:16 -05:00
Charlie Marsh
d65542c05e [ty] Make tuple intersection a fallible operation (#22094)
## Summary

This PR attempts to address a TODO in
https://github.com/astral-sh/ruff/pull/21965#discussion_r2635378498.
2026-01-06 10:47:04 -05:00
Aria Desires
98728b2c98 [ty] improve indented codefence rendering in docstrings (#22408)
By stripping leading indents from codefence lines to ensure they're
properly understood by markdown (but otherwise preserving the indent in
the codeblock so all the code renders roughly at the right indent).

As described in [this
comment](https://github.com/astral-sh/ty/issues/2352#issuecomment-3711686053)
this solution is very "do what I mean" for when a user has an explicit
markdown codeblock in e.g. a `Returns:` section which "has" to be
indented but that indent makes the verbatim codefence invalid markdown.

* Fixes https://github.com/astral-sh/ty/issues/2352
2026-01-06 10:44:31 -05:00
Dylan
924b2972f2 Update Black tests (#22405)
I am updating these because we didn't have test coverage for the
different handling of `fmt: skip` comments applied to multiple
statements on the same line. This is in preparation for #22119 (to show
before/after deviations).

Follows the same procedure as in #20794

Edit: As it happens, the new fixtures do not even cover the case
relevant to #22119 - they just deal with the already handled case of a
one-line compound statement. Nevertheless, it seems worthwhile to make
this update, especially since it uncovered a (possible?) bug.
2026-01-06 09:09:05 -06:00
Andrew Gallant
d035744959 [ty] Include = in completion suggestions in playground
This was an accidental omission in #21988 and identified in
astral-sh/ty#2203.
2026-01-06 09:26:29 -05:00
RasmusNygren
ce059c4857 [ty] Sort keyword argument completions higher (#22297) 2026-01-06 10:57:10 +00:00
Micha Reiser
acbc83d6d2 [ty] Fix stale semantic tokens after opening the same document with new content (#22414) 2026-01-06 11:52:51 +01:00
RasmusNygren
a9e5246786 [ty] Ensure the ty playground module is only ever loaded once (#22409) 2026-01-06 10:52:02 +01:00
Charlie Marsh
8b8b174e4f [ty] Add a diagnostic for @functools.total_ordering without a defined comparison method (#22183)
## Summary

This raises a `ValueError` at runtime:

```python
from functools import total_ordering

@total_ordering
class NoOrdering:
    def __eq__(self, other: object) -> bool:
        return True
```

Specifically:

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/functools.py", line 193, in total_ordering
    raise ValueError('must define at least one ordering operation: < > <= >=')
ValueError: must define at least one ordering operation: < > <= >=
```

See: https://github.com/astral-sh/ty/issues/1202.
2026-01-06 04:14:06 +00:00
Charlie Marsh
28fa02129b [ty] Add support for @total_ordering (#22181)
## Summary

We have some suppressions in the pyx codebase related to this, so wanted
to resolve.

Closes https://github.com/astral-sh/ty/issues/1202.
2026-01-05 22:47:03 -05:00
Brent Westbrook
a10e42294b [pylint] Demote PLW1510 fix to display-only (#22318)
Summary
--

Closes #17091. `PLW1510` checks for `subprocess.run` calls without a
`check`
keyword argument and previously had a safe fix to add `check=False`.
That's the
default value, so technically it preserved the code's behavior, but as
discussed
in #17091 and #17087, Ruff can't actually know what the author intended.

I don't think it hurts to keep this as a display-only fix instead of
removing it
entirely, but it definitely shouldn't be safe at the very least.

Test Plan
--

Existing tests
2026-01-05 19:36:16 -05:00
Amethyst Reese
12a4ca003f [flake8_print] better suggestion for basicConfig in T201 docs (#22101)
`logging.basicConfig` should not be called at a global module scope,
as that produces a race condition to configure logging based on which
module gets imported first.  Logging should instead be initialized
in an entrypoint to the program, either in a `main()` or in the
typical `if __name__ == "__main__"` block.
2026-01-05 11:42:47 -08:00
Charlie Marsh
60f7ec90ef Add a fast-test profile (#22382)
## Summary

We use this profile in uv to create success, as an optimization for the
iterative test loop. We include `opt-level=1` because it ends up being
"worth it" for testing (empirically), even though it means the build is
actually a big slower than `dev` (if you remove `opt-level=1`, clean
compile is about 22% faster than `dev`).

Here are some benchmarks I generated with Claude -- the main motivator
here is the incremental testing for `ty_python_semantic` which is 2.4x
faster:

### `ty_python_semantic`

Full test suite (471 tests):
| Scenario    | dev   | fast-test | Improvement |
|-------------|-------|------------|-------------|
| Clean       | 53s   | 49s        | 8% faster   |
| Incremental | 17.8s | 6.8s       | 2.4x faster |

Single test:
| Scenario    | dev   | fast-test | Improvement |
|-------------|-------|------------|-------------|
| Clean       | 42.5s | 55.3s      | 30% slower  |
| Incremental | 6.5s  | 6.1s       | ~same       |

### `ruff_linter`

Full test suite (2622 tests):
| Scenario    | dev   | fast-test | Improvement |
|-------------|-------|------------|-------------|
| Clean       | 31s   | 41s        | 32% slower  |
| Incremental | 11.9s | 10.5s      | 12% faster  |

Single test:
| Scenario    | dev  | fast-test | Improvement |
|-------------|------|------------|-------------|
| Clean       | 26s  | 36.5s      | 40% slower  |
| Incremental | 4.5s | 5.5s       | 22% slower  |
2026-01-05 19:35:43 +00:00
Jack O'Connor
922d964bcb [ty] emit diagnostics for method definitions and other invalid statements in TypedDict class bodies (#22351)
Fixes https://github.com/astral-sh/ty/issues/2277.
2026-01-05 11:28:04 -08:00
85 changed files with 3660 additions and 545 deletions

View File

@@ -10,6 +10,12 @@ Run all tests (using `nextest` for faster execution):
cargo nextest run
```
For faster test execution, use the `fast-test` profile which enables optimizations while retaining debug info:
```sh
cargo nextest run --cargo-profile fast-test
```
Run tests for a specific crate:
```sh

View File

@@ -335,6 +335,11 @@ strip = false
debug = "full"
lto = false
# Profile for faster iteration: applies minimal optimizations for faster tests.
[profile.fast-test]
inherits = "dev"
opt-level = 1
# The profile that 'cargo dist' will build with.
[profile.dist]
inherits = "release"

View File

@@ -526,7 +526,7 @@ impl VirtualFile {
}
/// Increments the revision of the underlying [`File`].
fn sync(&self, db: &mut dyn Db) {
pub fn sync(&self, db: &mut dyn Db) {
let file = self.0;
tracing::debug!("Updating the revision of `{}`", file.path(db));
let current_revision = file.revision(db);

View File

@@ -36,13 +36,16 @@ use crate::{Fix, FixAvailability, Violation};
/// ```python
/// import logging
///
/// logging.basicConfig(level=logging.INFO)
/// logger = logging.getLogger(__name__)
///
///
/// def sum_less_than_four(a, b):
/// logger.debug("Calling sum_less_than_four")
/// return a + b < 4
///
///
/// if __name__ == "__main__":
/// logging.basicConfig(level=logging.INFO)
/// ```
///
/// ## Fix safety

View File

@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::{AlwaysFixableViolation, Applicability, Fix};
use crate::{Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `subprocess.run` without an explicit `check` argument.
@@ -39,9 +39,12 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for function calls that contain
/// `**kwargs`, as adding a `check` keyword argument to such a call may lead
/// to a duplicate keyword argument error.
///
/// This rule's fix is marked as display-only because it's not clear whether the
/// potential exception was meant to be ignored by setting `check=False` or if
/// the author simply forgot to include `check=True`. The fix adds
/// `check=False`, making the existing behavior explicit but possibly masking
/// the original intention.
///
/// ## References
/// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run)
@@ -49,14 +52,18 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
#[violation_metadata(stable_since = "v0.0.285")]
pub(crate) struct SubprocessRunWithoutCheck;
impl AlwaysFixableViolation for SubprocessRunWithoutCheck {
impl Violation for SubprocessRunWithoutCheck {
// The fix is always set on the diagnostic, but display-only fixes aren't
// considered "fixable" in the tests.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`subprocess.run` without explicit `check` argument".to_string()
}
fn fix_title(&self) -> String {
"Add explicit `check=False`".to_string()
fn fix_title(&self) -> Option<String> {
Some("Add explicit `check=False`".to_string())
}
}
@@ -74,20 +81,11 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa
if call.arguments.find_keyword("check").is_none() {
let mut diagnostic =
checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range());
diagnostic.set_fix(Fix::applicable_edit(
add_argument("check=False", &call.arguments, checker.tokens()),
// If the function call contains `**kwargs`, mark the fix as unsafe.
if call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
diagnostic.set_fix(Fix::display_only_edit(add_argument(
"check=False",
&call.arguments,
checker.tokens(),
)));
}
}
}

View File

@@ -19,6 +19,7 @@ help: Add explicit `check=False`
5 | subprocess.run("ls", shell=True)
6 | subprocess.run(
7 | ["ls"],
note: This is a display-only fix and is likely to be incorrect
PLW1510 [*] `subprocess.run` without explicit `check` argument
--> subprocess_run_without_check.py:5:1
@@ -39,6 +40,7 @@ help: Add explicit `check=False`
6 | subprocess.run(
7 | ["ls"],
8 | shell=False,
note: This is a display-only fix and is likely to be incorrect
PLW1510 [*] `subprocess.run` without explicit `check` argument
--> subprocess_run_without_check.py:6:1
@@ -59,6 +61,7 @@ help: Add explicit `check=False`
9 | )
10 | subprocess.run(["ls"], **kwargs)
11 |
note: This is a display-only fix and is likely to be incorrect
PLW1510 [*] `subprocess.run` without explicit `check` argument
--> subprocess_run_without_check.py:10:1
@@ -79,4 +82,4 @@ help: Add explicit `check=False`
11 |
12 | # Non-errors.
13 | subprocess.run("ls", check=True)
note: This is an unsafe fix and may change runtime behavior
note: This is a display-only fix and is likely to be incorrect

View File

@@ -91,20 +91,22 @@ def example(session):
.all()
# fmt: on
def off_and_on_without_data():
"""All comments here are technically on the same prefix.
The comments between will be formatted. This is a known limitation.
"""
"""Test that comment-only fmt:off/on blocks preserve formatting."""
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
#hey, that won't work
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
pass
def on_and_off_broken():
"""Another known limitation."""
def on_and_off_with_comment_only_blocks():
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
# fmt: on
# fmt: off
this=should.not_be.formatted()
@@ -113,7 +115,16 @@ def on_and_off_broken():
now . considers . multiple . fmt . directives . within . one . prefix
# fmt: on
# fmt: off
# ...but comments still get reformatted even though they should not be
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
def long_lines():
if True:
@@ -178,6 +189,50 @@ cfg.rule(
# fmt: on
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5
)
# Test comment-only blocks at file level with various spacing patterns
# fmt: off
#nospace
# twospaces
# fmt: on
# fmt: off
#nospaceatall
#extraspaces
#evenmorespaces
# fmt: on
# fmt: off
# fmt: on
# fmt: off
#SBATCH --job-name=test
#SBATCH --output=test.out
# fmt: on
# fmt: off
#first
#second
# fmt: on
# fmt: off
#!@#$%^&*()
#<=>+-*/
# fmt: on
# fmt: off
#x=1+2
#y = 3
#z = 4
# fmt: on
# fmt: off
yield 'hello'
# No formatting to the end of the file

View File

@@ -112,29 +112,42 @@ def example(session):
def off_and_on_without_data():
"""All comments here are technically on the same prefix.
The comments between will be formatted. This is a known limitation.
"""
"""Test that comment-only fmt:off/on blocks preserve formatting."""
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
# hey, that won't work
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
pass
def on_and_off_broken():
"""Another known limitation."""
def on_and_off_with_comment_only_blocks():
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
# fmt: on
# fmt: off
this=should.not_be.formatted()
and_=indeed . it is not formatted
because . the . handling . inside . generate_ignored_nodes()
now . considers . multiple . fmt . directives . within . one . prefix
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
# ...but comments still get reformatted even though they should not be
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
@@ -211,6 +224,50 @@ cfg.rule(
# fmt: on
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5,
)
# Test comment-only blocks at file level with various spacing patterns
# fmt: off
#nospace
# twospaces
# fmt: on
# fmt: off
#nospaceatall
#extraspaces
#evenmorespaces
# fmt: on
# fmt: off
# fmt: on
# fmt: off
#SBATCH --job-name=test
#SBATCH --output=test.out
# fmt: on
# fmt: off
#first
#second
# fmt: on
# fmt: off
#!@#$%^&*()
#<=>+-*/
# fmt: on
# fmt: off
#x=1+2
#y = 3
#z = 4
# fmt: on
# fmt: off
yield 'hello'
# No formatting to the end of the file

View File

@@ -1,8 +1,21 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True: print("this"); print("that") # fmt: skip
while True: print("loop"); break # fmt: skip
for x in [1, 2]: print(x); print("done") # fmt: skip
def f(x: int): return x # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
v = (
foo_dict # fmt: skip
.setdefault("a", {})
.setdefault("b", {})
.setdefault("c", {})
.setdefault("d", {})
.setdefault("e", {})
)

View File

@@ -1,8 +1,21 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True: print("this"); print("that") # fmt: skip
while True: print("loop"); break # fmt: skip
for x in [1, 2]: print(x); print("done") # fmt: skip
def f(x: int): return x # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
v = (
foo_dict # fmt: skip
.setdefault("a", {})
.setdefault("b", {})
.setdefault("c", {})
.setdefault("d", {})
.setdefault("e", {})
)

View File

@@ -4,3 +4,84 @@ def foo():
# comment 1 # fmt: skip
# comment 2
[
(1, 2),
# # fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# fmt: on
(5, 6),
]
if False:
# fmt: off # some other comment
pass

View File

@@ -4,3 +4,84 @@ def foo():
# comment 1 # fmt: skip
# comment 2
[
(1, 2),
# # fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# fmt: on
(5, 6),
]
if False:
# fmt: off # some other comment
pass

View File

@@ -0,0 +1 @@
{"preview": "enabled"}

View File

@@ -0,0 +1,8 @@
with open("file.txt") as f: content = f.read() # fmt: skip
# Ideally, only the last line would be ignored
# But ignoring only part of the asexpr_test causes a parse error
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
with open (
"file.txt" ,
) as f: content = f.read() # fmt: skip

View File

@@ -0,0 +1,8 @@
with open("file.txt") as f: content = f.read() # fmt: skip
# Ideally, only the last line would be ignored
# But ignoring only part of the asexpr_test causes a parse error
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
with open (
"file.txt" ,
) as f: content = f.read() # fmt: skip

View File

@@ -0,0 +1 @@
{"preview": "enabled"}

View File

@@ -0,0 +1,28 @@
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{"foo": "bar"},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar"},
)
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{"foo": "bar",},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar",},
)

View File

@@ -0,0 +1,32 @@
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{"foo": "bar"},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar"},
)
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{
"foo": "bar",
},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{
"foo": "bar",
},
)

View File

@@ -1,4 +1,4 @@
a = "this is some code"
b = 5 #fmt:skip
b = 5 # fmt:skip
c = 9 #fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" #fmt:skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip

View File

@@ -1,4 +1,4 @@
a = "this is some code"
b = 5 # fmt:skip
c = 9 # fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
b = 5 # fmt:skip
c = 9 #fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip

View File

@@ -0,0 +1,19 @@
# Multiple fmt: skip in multi-part if-clause
class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3
def test():
if (
"cond1" == "cond1"
and "cond2" == "cond2"
and 1 in (
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3, # fmt: skip
) # fmt: skip
):
return True
return False

View File

@@ -0,0 +1,19 @@
# Multiple fmt: skip in multi-part if-clause
class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3
def test():
if (
"cond1" == "cond1"
and "cond2" == "cond2"
and 1 in (
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3, # fmt: skip
) # fmt: skip
):
return True
return False

View File

@@ -0,0 +1,35 @@
# Multiple fmt: skip on string literals
a = (
"this should " # fmt: skip
"be fine"
)
b = (
"this is " # fmt: skip
"not working" # fmt: skip
)
c = (
"and neither " # fmt: skip
"is this " # fmt: skip
"working"
)
d = (
"nor "
"is this " # fmt: skip
"working" # fmt: skip
)
e = (
"and this " # fmt: skip
"is definitely "
"not working" # fmt: skip
)
# Dictionary entries with fmt: skip (covers issue with long lines)
hotkeys = {
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
}

View File

@@ -0,0 +1,35 @@
# Multiple fmt: skip on string literals
a = (
"this should " # fmt: skip
"be fine"
)
b = (
"this is " # fmt: skip
"not working" # fmt: skip
)
c = (
"and neither " # fmt: skip
"is this " # fmt: skip
"working"
)
d = (
"nor "
"is this " # fmt: skip
"working" # fmt: skip
)
e = (
"and this " # fmt: skip
"is definitely "
"not working" # fmt: skip
)
# Dictionary entries with fmt: skip (covers issue with long lines)
hotkeys = {
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
}

View File

@@ -0,0 +1,24 @@
# Test that Jupytext markdown comments are preserved before fmt:off/on blocks
# %% [markdown]
# fmt: off
# fmt: on
# Also test with other comments
# Some comment
# %% [markdown]
# Another comment
# fmt: off
x = 1
# fmt: on
# Test multiple markdown comments
# %% [markdown]
# First markdown
# %% [code]
# Code cell
# fmt: off
y = 2
# fmt: on

View File

@@ -0,0 +1,24 @@
# Test that Jupytext markdown comments are preserved before fmt:off/on blocks
# %% [markdown]
# fmt: off
# fmt: on
# Also test with other comments
# Some comment
# %% [markdown]
# Another comment
# fmt: off
x = 1
# fmt: on
# Test multiple markdown comments
# %% [markdown]
# First markdown
# %% [code]
# Code cell
# fmt: off
y = 2
# fmt: on

View File

@@ -0,0 +1 @@
{"target_version": "3.14"}

View File

@@ -0,0 +1,40 @@
x = t"foo"
x = t'foo {{ {2 + 2}bar {{ baz'
x = t"foo {f'abc'} bar"
x = t"""foo {{ a
foo {2 + 2}bar {{ baz
x = f"foo {{ {
2 + 2 # comment
}bar"
{{ baz
}} buzz
{print("abc" + "def"
)}
abc"""
t'{(abc:=10)}'
t'''This is a really long string, but just make sure that you reflow tstrings {
2+2:d
}'''
t'This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}'
t"{ 2 + 2 = }"
t'{
X
!r
}'
tr'\{{\}}'
t'''
WITH {f'''
{1}_cte AS ()'''}
'''

View File

@@ -0,0 +1,40 @@
x = t"foo"
x = t"foo {{ {2 + 2}bar {{ baz"
x = t"foo {f'abc'} bar"
x = t"""foo {{ a
foo {2 + 2}bar {{ baz
x = f"foo {{ {
2 + 2 # comment
}bar"
{{ baz
}} buzz
{print("abc" + "def"
)}
abc"""
t"{(abc:=10)}"
t"""This is a really long string, but just make sure that you reflow tstrings {
2+2:d
}"""
t"This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}"
t"{ 2 + 2 = }"
t"{
X
!r
}"
rt"\{{\}}"
t"""
WITH {f'''
{1}_cte AS ()'''}
"""

View File

@@ -0,0 +1 @@
{"preview": "enabled"}

View File

@@ -0,0 +1,19 @@
# Regression test for https://github.com/psf/black/issues/3438
import ast
import collections # fmt: skip
import dataclasses
# fmt: off
import os
# fmt: on
import pathlib
import re # fmt: skip
import secrets
# fmt: off
import sys
# fmt: on
import tempfile
import zoneinfo

View File

@@ -0,0 +1,19 @@
# Regression test for https://github.com/psf/black/issues/3438
import ast
import collections # fmt: skip
import dataclasses
# fmt: off
import os
# fmt: on
import pathlib
import re # fmt: skip
import secrets
# fmt: off
import sys
# fmt: on
import tempfile
import zoneinfo

View File

@@ -156,24 +156,6 @@ Please use `--build-option` instead,
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
"""
this_will_become_one_line = (
"a"
"b"
"c"
)
this_will_stay_on_three_lines = (
"a" # comment
"b"
"c"
)
this_will_also_become_one_line = ( # comment
"a"
"b"
"c"
)
assert some_var == expected_result, """
test
"""

View File

@@ -198,16 +198,6 @@ Please use `--build-option` instead,
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
"""
this_will_become_one_line = "abc"
this_will_stay_on_three_lines = (
"a" # comment
"b"
"c"
)
this_will_also_become_one_line = "abc" # comment
assert some_var == expected_result, """
test
"""

View File

@@ -0,0 +1,10 @@
def foo(
a, #type:int
b, #type: str
c, # type: List[int]
d, # type: Dict[int, str]
e, # type: ignore
f, # type : ignore
g, # type : ignore
):
pass

View File

@@ -0,0 +1,10 @@
def foo(
a, # type: int
b, # type: str
c, # type: List[int]
d, # type: Dict[int, str]
e, # type: ignore
f, # type : ignore
g, # type : ignore
):
pass

View File

@@ -0,0 +1 @@
{"preview": "enabled"}

View File

@@ -0,0 +1,16 @@
# Remove unnecessary parentheses from LHS of assignments
def a():
return [1, 2, 3]
# Single variable with unnecessary parentheses
(b) = a()[0]
# Tuple unpacking with unnecessary parentheses
(c, *_) = a()
# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs

View File

@@ -0,0 +1,16 @@
# Remove unnecessary parentheses from LHS of assignments
def a():
return [1, 2, 3]
# Single variable with unnecessary parentheses
b = a()[0]
# Tuple unpacking with unnecessary parentheses
c, *_ = a()
# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtonoff.py
---
## Input
@@ -98,20 +97,22 @@ def example(session):
.all()
# fmt: on
def off_and_on_without_data():
"""All comments here are technically on the same prefix.
The comments between will be formatted. This is a known limitation.
"""
"""Test that comment-only fmt:off/on blocks preserve formatting."""
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
#hey, that won't work
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
pass
def on_and_off_broken():
"""Another known limitation."""
def on_and_off_with_comment_only_blocks():
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
# fmt: on
# fmt: off
this=should.not_be.formatted()
@@ -120,7 +121,16 @@ def on_and_off_broken():
now . considers . multiple . fmt . directives . within . one . prefix
# fmt: on
# fmt: off
# ...but comments still get reformatted even though they should not be
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
def long_lines():
if True:
@@ -185,6 +195,50 @@ cfg.rule(
# fmt: on
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5
)
# Test comment-only blocks at file level with various spacing patterns
# fmt: off
#nospace
# twospaces
# fmt: on
# fmt: off
#nospaceatall
#extraspaces
#evenmorespaces
# fmt: on
# fmt: off
# fmt: on
# fmt: off
#SBATCH --job-name=test
#SBATCH --output=test.out
# fmt: on
# fmt: off
#first
#second
# fmt: on
# fmt: off
#!@#$%^&*()
#<=>+-*/
# fmt: on
# fmt: off
#x=1+2
#y = 3
#z = 4
# fmt: on
# fmt: off
yield 'hello'
# No formatting to the end of the file
@@ -225,28 +279,16 @@ d={'a':1,
# fmt: on
goes + here,
andhere,
@@ -118,8 +119,10 @@
"""
# fmt: off
- # hey, that won't work
+ #hey, that won't work
+
+
# fmt: on
pass
@@ -134,7 +137,7 @@
@@ -136,7 +137,7 @@
and_=indeed . it is not formatted
because . the . handling . inside . generate_ignored_nodes()
now . considers . multiple . fmt . directives . within . one . prefix
# fmt: on
-
+ # fmt: on
# fmt: off
- # ...but comments still get reformatted even though they should not be
+ # ...but comments still get reformatted even though they should not be
#should not be formatted
# fmt: on
@@ -174,14 +177,18 @@
@@ -187,14 +188,18 @@
$
""",
# fmt: off
@@ -387,22 +429,24 @@ def example(session):
def off_and_on_without_data():
"""All comments here are technically on the same prefix.
The comments between will be formatted. This is a known limitation.
"""
"""Test that comment-only fmt:off/on blocks preserve formatting."""
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#hey, that won't work
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
pass
def on_and_off_broken():
"""Another known limitation."""
def on_and_off_with_comment_only_blocks():
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
# fmt: on
# fmt: off
this=should.not_be.formatted()
@@ -411,7 +455,16 @@ def on_and_off_broken():
now . considers . multiple . fmt . directives . within . one . prefix
# fmt: on
# fmt: off
# ...but comments still get reformatted even though they should not be
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
@@ -492,6 +545,50 @@ cfg.rule(
# fmt: on
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5,
)
# Test comment-only blocks at file level with various spacing patterns
# fmt: off
#nospace
# twospaces
# fmt: on
# fmt: off
#nospaceatall
#extraspaces
#evenmorespaces
# fmt: on
# fmt: off
# fmt: on
# fmt: off
#SBATCH --job-name=test
#SBATCH --output=test.out
# fmt: on
# fmt: off
#first
#second
# fmt: on
# fmt: off
#!@#$%^&*()
#<=>+-*/
# fmt: on
# fmt: off
#x=1+2
#y = 3
#z = 4
# fmt: on
# fmt: off
yield 'hello'
# No formatting to the end of the file
@@ -617,29 +714,42 @@ def example(session):
def off_and_on_without_data():
"""All comments here are technically on the same prefix.
The comments between will be formatted. This is a known limitation.
"""
"""Test that comment-only fmt:off/on blocks preserve formatting."""
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
# hey, that won't work
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
pass
def on_and_off_broken():
"""Another known limitation."""
def on_and_off_with_comment_only_blocks():
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
# fmt: on
# fmt: off
this=should.not_be.formatted()
and_=indeed . it is not formatted
because . the . handling . inside . generate_ignored_nodes()
now . considers . multiple . fmt . directives . within . one . prefix
# fmt: off
#should not be formatted
# fmt: on
# fmt: off
# ...but comments still get reformatted even though they should not be
#should not be formatted
# fmt: on
# fmt: off
#should not be formatted
#should not be formatted #also should not be formatted
# fmt: on
@@ -716,6 +826,50 @@ cfg.rule(
# fmt: on
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5,
)
# Test comment-only blocks at file level with various spacing patterns
# fmt: off
#nospace
# twospaces
# fmt: on
# fmt: off
#nospaceatall
#extraspaces
#evenmorespaces
# fmt: on
# fmt: off
# fmt: on
# fmt: off
#SBATCH --job-name=test
#SBATCH --output=test.out
# fmt: on
# fmt: off
#first
#second
# fmt: on
# fmt: off
#!@#$%^&*()
#<=>+-*/
# fmt: on
# fmt: off
#x=1+2
#y = 3
#z = 4
# fmt: on
# fmt: off
yield 'hello'
# No formatting to the end of the file

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip10.py
---
## Input
@@ -8,11 +7,24 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmt
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True: print("this"); print("that") # fmt: skip
while True: print("loop"); break # fmt: skip
for x in [1, 2]: print(x); print("done") # fmt: skip
def f(x: int): return x # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
v = (
foo_dict # fmt: skip
.setdefault("a", {})
.setdefault("b", {})
.setdefault("c", {})
.setdefault("d", {})
.setdefault("e", {})
)
```
## Black Differences
@@ -20,19 +32,30 @@ b = [c for c in "A very long string that would normally generate some kind of co
```diff
--- Black
+++ Ruff
@@ -1,8 +1,10 @@
@@ -1,15 +1,20 @@
def foo(): return "mock" # fmt: skip
+
+
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True: print("this"); print("that") # fmt: skip
while True: print("loop"); break # fmt: skip
for x in [1, 2]: print(x); print("done") # fmt: skip
-def f(x: int): return x # fmt: skip
-j = 1 # fmt: skip
+
+def f(x: int): return x # fmt: skip
+
+
+j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
-b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
+b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
v = (
foo_dict # fmt: skip
```
## Ruff Output
@@ -43,11 +66,27 @@ def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True: print("this"); print("that") # fmt: skip
while True: print("loop"); break # fmt: skip
for x in [1, 2]: print(x); print("done") # fmt: skip
def f(x: int): return x # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
v = (
foo_dict # fmt: skip
.setdefault("a", {})
.setdefault("b", {})
.setdefault("c", {})
.setdefault("d", {})
.setdefault("e", {})
)
```
## Black Output
@@ -56,9 +95,22 @@ b = [c for c in "A very long string that would normally generate some kind of co
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True: print("this"); print("that") # fmt: skip
while True: print("loop"); break # fmt: skip
for x in [1, 2]: print(x); print("done") # fmt: skip
def f(x: int): return x # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
v = (
foo_dict # fmt: skip
.setdefault("a", {})
.setdefault("b", {})
.setdefault("c", {})
.setdefault("d", {})
.setdefault("e", {})
)
```

View File

@@ -0,0 +1,321 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
def foo():
pass
# comment 1 # fmt: skip
# comment 2
[
(1, 2),
# # fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# fmt: on
(5, 6),
]
if False:
# fmt: off # some other comment
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -46,8 +46,7 @@
[
(1, 2),
# # fmt: off
- (3,
- 4),
+ (3, 4),
# # fmt: on
(5, 6),
]
@@ -55,8 +54,7 @@
[
(1, 2),
# # fmt: off
- (3,
- 4),
+ (3, 4),
# fmt: on
(5, 6),
]
@@ -65,8 +63,7 @@
[
(1, 2),
# fmt: off
- (3,
- 4),
+ (3, 4),
# # fmt: on
(5, 6),
]
@@ -75,8 +72,7 @@
[
(1, 2),
# fmt: off
- (3,
- 4),
+ (3, 4),
# fmt: on
(5, 6),
]
```
## Ruff Output
```python
def foo():
pass
# comment 1 # fmt: skip
# comment 2
[
(1, 2),
# # fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3, 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3, 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3, 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3, 4),
# fmt: on
(5, 6),
]
if False:
# fmt: off # some other comment
pass
```
## Black Output
```python
def foo():
pass
# comment 1 # fmt: skip
# comment 2
[
(1, 2),
# # fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
# (3,
# 4),
# fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# # fmt: off
(3,
4),
# fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# # fmt: on
(5, 6),
]
[
(1, 2),
# fmt: off
(3,
4),
# fmt: on
(5, 6),
]
if False:
# fmt: off # some other comment
pass
```

View File

@@ -0,0 +1,59 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
with open("file.txt") as f: content = f.read() # fmt: skip
# Ideally, only the last line would be ignored
# But ignoring only part of the asexpr_test causes a parse error
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
with open (
"file.txt" ,
) as f: content = f.read() # fmt: skip
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,8 +1,8 @@
-with open("file.txt") as f: content = f.read() # fmt: skip
+with open("file.txt") as f: content = f.read() # fmt: skip
# Ideally, only the last line would be ignored
# But ignoring only part of the asexpr_test causes a parse error
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
with open (
"file.txt" ,
-) as f: content = f.read() # fmt: skip
+) as f: content = f.read() # fmt: skip
```
## Ruff Output
```python
with open("file.txt") as f: content = f.read() # fmt: skip
# Ideally, only the last line would be ignored
# But ignoring only part of the asexpr_test causes a parse error
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
with open (
"file.txt" ,
) as f: content = f.read() # fmt: skip
```
## Black Output
```python
with open("file.txt") as f: content = f.read() # fmt: skip
# Ideally, only the last line would be ignored
# But ignoring only part of the asexpr_test causes a parse error
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
with open (
"file.txt" ,
) as f: content = f.read() # fmt: skip
```

View File

@@ -0,0 +1,149 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{"foo": "bar"},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar"},
)
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{"foo": "bar",},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar",},
)
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,5 +1,9 @@
t = (
- {"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
+ {
+ "foo": "very long string",
+ "bar": "another very long string",
+ "baz": "we should run out of space by now",
+ }, # fmt: skip
{"foo": "bar"},
)
@@ -14,8 +18,12 @@
t = (
- {"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{
+ "foo": "very long string",
+ "bar": "another very long string",
+ "baz": "we should run out of space by now",
+ }, # fmt: skip
+ {
"foo": "bar",
},
)
```
## Ruff Output
```python
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar"},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar"},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{
"foo": "bar",
},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{
"foo": "bar",
},
)
```
## Black Output
```python
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{"foo": "bar"},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{"foo": "bar"},
)
t = (
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
{
"foo": "bar",
},
)
t = (
{
"foo": "very long string",
"bar": "another very long string",
"baz": "we should run out of space by now",
}, # fmt: skip
{
"foo": "bar",
},
)
```

View File

@@ -0,0 +1,43 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
a = "this is some code"
b = 5 # fmt:skip
c = 9 #fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,4 +1,4 @@
a = "this is some code"
-b = 5 # fmt:skip
-c = 9 #fmt: skip
+b = 5 # fmt:skip
+c = 9 # fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
```
## Ruff Output
```python
a = "this is some code"
b = 5 # fmt:skip
c = 9 # fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
```
## Black Output
```python
a = "this is some code"
b = 5 # fmt:skip
c = 9 #fmt: skip
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
```

View File

@@ -0,0 +1,98 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
# Multiple fmt: skip in multi-part if-clause
class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3
def test():
if (
"cond1" == "cond1"
and "cond2" == "cond2"
and 1 in (
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3, # fmt: skip
) # fmt: skip
):
return True
return False
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -9,11 +9,12 @@
if (
"cond1" == "cond1"
and "cond2" == "cond2"
- and 1 in (
+ and 1
+ in (
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
- ClassWithALongName.Constant3, # fmt: skip
- ) # fmt: skip
+ ClassWithALongName.Constant3, # fmt: skip
+ ) # fmt: skip
):
return True
return False
```
## Ruff Output
```python
# Multiple fmt: skip in multi-part if-clause
class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3
def test():
if (
"cond1" == "cond1"
and "cond2" == "cond2"
and 1
in (
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3, # fmt: skip
) # fmt: skip
):
return True
return False
```
## Black Output
```python
# Multiple fmt: skip in multi-part if-clause
class ClassWithALongName:
Constant1 = 1
Constant2 = 2
Constant3 = 3
def test():
if (
"cond1" == "cond1"
and "cond2" == "cond2"
and 1 in (
ClassWithALongName.Constant1,
ClassWithALongName.Constant2,
ClassWithALongName.Constant3, # fmt: skip
) # fmt: skip
):
return True
return False
```

View File

@@ -0,0 +1,148 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
# Multiple fmt: skip on string literals
a = (
"this should " # fmt: skip
"be fine"
)
b = (
"this is " # fmt: skip
"not working" # fmt: skip
)
c = (
"and neither " # fmt: skip
"is this " # fmt: skip
"working"
)
d = (
"nor "
"is this " # fmt: skip
"working" # fmt: skip
)
e = (
"and this " # fmt: skip
"is definitely "
"not working" # fmt: skip
)
# Dictionary entries with fmt: skip (covers issue with long lines)
hotkeys = {
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
}
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -29,7 +29,11 @@
# Dictionary entries with fmt: skip (covers issue with long lines)
hotkeys = {
- "editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
- "editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
- "editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
+ "editor:swap-line-down": [
+ {"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}
+ ], # fmt: skip
+ "editor:swap-line-up": [
+ {"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}
+ ], # fmt: skip
+ "editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
}
```
## Ruff Output
```python
# Multiple fmt: skip on string literals
a = (
"this should " # fmt: skip
"be fine"
)
b = (
"this is " # fmt: skip
"not working" # fmt: skip
)
c = (
"and neither " # fmt: skip
"is this " # fmt: skip
"working"
)
d = (
"nor "
"is this " # fmt: skip
"working" # fmt: skip
)
e = (
"and this " # fmt: skip
"is definitely "
"not working" # fmt: skip
)
# Dictionary entries with fmt: skip (covers issue with long lines)
hotkeys = {
"editor:swap-line-down": [
{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}
], # fmt: skip
"editor:swap-line-up": [
{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}
], # fmt: skip
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
}
```
## Black Output
```python
# Multiple fmt: skip on string literals
a = (
"this should " # fmt: skip
"be fine"
)
b = (
"this is " # fmt: skip
"not working" # fmt: skip
)
c = (
"and neither " # fmt: skip
"is this " # fmt: skip
"working"
)
d = (
"nor "
"is this " # fmt: skip
"working" # fmt: skip
)
e = (
"and this " # fmt: skip
"is definitely "
"not working" # fmt: skip
)
# Dictionary entries with fmt: skip (covers issue with long lines)
hotkeys = {
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
}
```

View File

@@ -0,0 +1,188 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
x = t"foo"
x = t'foo {{ {2 + 2}bar {{ baz'
x = t"foo {f'abc'} bar"
x = t"""foo {{ a
foo {2 + 2}bar {{ baz
x = f"foo {{ {
2 + 2 # comment
}bar"
{{ baz
}} buzz
{print("abc" + "def"
)}
abc"""
t'{(abc:=10)}'
t'''This is a really long string, but just make sure that you reflow tstrings {
2+2:d
}'''
t'This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}'
t"{ 2 + 2 = }"
t'{
X
!r
}'
tr'\{{\}}'
t'''
WITH {f'''
{1}_cte AS ()'''}
'''
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -7,34 +7,32 @@
foo {2 + 2}bar {{ baz
x = f"foo {{ {
- 2 + 2 # comment
- }bar"
+ 2 + 2 # comment
+}bar"
{{ baz
}} buzz
- {print("abc" + "def"
-)}
+ {print("abc" + "def")}
abc"""
-t"{(abc:=10)}"
+t"{(abc := 10)}"
t"""This is a really long string, but just make sure that you reflow tstrings {
- 2+2:d
+ 2 + 2:d
}"""
-t"This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}"
+t"This is a really long string, but just make sure that you reflow tstrings correctly {2 + 2:d}"
t"{ 2 + 2 = }"
-t"{
-X
-!r
-}"
+t"{X!r}"
rt"\{{\}}"
t"""
- WITH {f'''
- {1}_cte AS ()'''}
+ WITH {
+ f'''
+ {1}_cte AS ()'''
+}
"""
```
## Ruff Output
```python
x = t"foo"
x = t"foo {{ {2 + 2}bar {{ baz"
x = t"foo {f'abc'} bar"
x = t"""foo {{ a
foo {2 + 2}bar {{ baz
x = f"foo {{ {
2 + 2 # comment
}bar"
{{ baz
}} buzz
{print("abc" + "def")}
abc"""
t"{(abc := 10)}"
t"""This is a really long string, but just make sure that you reflow tstrings {
2 + 2:d
}"""
t"This is a really long string, but just make sure that you reflow tstrings correctly {2 + 2:d}"
t"{ 2 + 2 = }"
t"{X!r}"
rt"\{{\}}"
t"""
WITH {
f'''
{1}_cte AS ()'''
}
"""
```
## Black Output
```python
x = t"foo"
x = t"foo {{ {2 + 2}bar {{ baz"
x = t"foo {f'abc'} bar"
x = t"""foo {{ a
foo {2 + 2}bar {{ baz
x = f"foo {{ {
2 + 2 # comment
}bar"
{{ baz
}} buzz
{print("abc" + "def"
)}
abc"""
t"{(abc:=10)}"
t"""This is a really long string, but just make sure that you reflow tstrings {
2+2:d
}"""
t"This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}"
t"{ 2 + 2 = }"
t"{
X
!r
}"
rt"\{{\}}"
t"""
WITH {f'''
{1}_cte AS ()'''}
"""
```

View File

@@ -0,0 +1,90 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
# Regression test for https://github.com/psf/black/issues/3438
import ast
import collections # fmt: skip
import dataclasses
# fmt: off
import os
# fmt: on
import pathlib
import re # fmt: skip
import secrets
# fmt: off
import sys
# fmt: on
import tempfile
import zoneinfo
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -3,6 +3,7 @@
import ast
import collections # fmt: skip
import dataclasses
+
# fmt: off
import os
# fmt: on
```
## Ruff Output
```python
# Regression test for https://github.com/psf/black/issues/3438
import ast
import collections # fmt: skip
import dataclasses
# fmt: off
import os
# fmt: on
import pathlib
import re # fmt: skip
import secrets
# fmt: off
import sys
# fmt: on
import tempfile
import zoneinfo
```
## Black Output
```python
# Regression test for https://github.com/psf/black/issues/3438
import ast
import collections # fmt: skip
import dataclasses
# fmt: off
import os
# fmt: on
import pathlib
import re # fmt: skip
import secrets
# fmt: off
import sys
# fmt: on
import tempfile
import zoneinfo
```

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_multiline_strings.py
---
## Input
@@ -163,24 +162,6 @@ Please use `--build-option` instead,
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
"""
this_will_become_one_line = (
"a"
"b"
"c"
)
this_will_stay_on_three_lines = (
"a" # comment
"b"
"c"
)
this_will_also_become_one_line = ( # comment
"a"
"b"
"c"
)
assert some_var == expected_result, """
test
"""
@@ -421,18 +402,7 @@ a = b if """
[
"""cow
moos""",
@@ -206,7 +245,9 @@
"c"
)
-this_will_also_become_one_line = "abc" # comment
+this_will_also_become_one_line = ( # comment
+ "abc"
+)
assert some_var == expected_result, """
test
@@ -224,10 +265,8 @@
@@ -214,10 +253,8 @@
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
),
@@ -445,7 +415,7 @@ a = b if """
},
}
@@ -246,14 +285,12 @@
@@ -236,14 +273,12 @@
a
a"""
),
@@ -706,18 +676,6 @@ Please use `--build-option` instead,
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
"""
this_will_become_one_line = "abc"
this_will_stay_on_three_lines = (
"a" # comment
"b"
"c"
)
this_will_also_become_one_line = ( # comment
"abc"
)
assert some_var == expected_result, """
test
"""
@@ -1028,16 +986,6 @@ Please use `--build-option` instead,
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
"""
this_will_become_one_line = "abc"
this_will_stay_on_three_lines = (
"a" # comment
"b"
"c"
)
this_will_also_become_one_line = "abc" # comment
assert some_var == expected_result, """
test
"""

View File

@@ -0,0 +1,67 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
def foo(
a, #type:int
b, #type: str
c, # type: List[int]
d, # type: Dict[int, str]
e, # type: ignore
f, # type : ignore
g, # type : ignore
):
pass
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -1,9 +1,9 @@
def foo(
- a, # type: int
+ a, # type:int
b, # type: str
c, # type: List[int]
- d, # type: Dict[int, str]
- e, # type: ignore
+ d, # type: Dict[int, str]
+ e, # type: ignore
f, # type : ignore
g, # type : ignore
):
```
## Ruff Output
```python
def foo(
a, # type:int
b, # type: str
c, # type: List[int]
d, # type: Dict[int, str]
e, # type: ignore
f, # type : ignore
g, # type : ignore
):
pass
```
## Black Output
```python
def foo(
a, # type: int
b, # type: str
c, # type: List[int]
d, # type: Dict[int, str]
e, # type: ignore
f, # type : ignore
g, # type : ignore
):
pass
```

View File

@@ -0,0 +1,85 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
---
## Input
```python
# Remove unnecessary parentheses from LHS of assignments
def a():
return [1, 2, 3]
# Single variable with unnecessary parentheses
(b) = a()[0]
# Tuple unpacking with unnecessary parentheses
(c, *_) = a()
# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs
```
## Black Differences
```diff
--- Black
+++ Ruff
@@ -6,10 +6,10 @@
# Single variable with unnecessary parentheses
-b = a()[0]
+(b) = a()[0]
# Tuple unpacking with unnecessary parentheses
-c, *_ = a()
+(c, *_) = a()
# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
```
## Ruff Output
```python
# Remove unnecessary parentheses from LHS of assignments
def a():
return [1, 2, 3]
# Single variable with unnecessary parentheses
(b) = a()[0]
# Tuple unpacking with unnecessary parentheses
(c, *_) = a()
# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs
```
## Black Output
```python
# Remove unnecessary parentheses from LHS of assignments
def a():
return [1, 2, 3]
# Single variable with unnecessary parentheses
b = a()[0]
# Tuple unpacking with unnecessary parentheses
c, *_ = a()
# These should not be changed - parentheses are necessary
(d,) = a() # single-element tuple
e = (1 + 2) * 3 # RHS has precedence needs
```

275
crates/ty/docs/rules.md generated
View File

@@ -8,7 +8,7 @@
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L538" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541" target="_blank">View source</a>
</small>
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L137" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L140" target="_blank">View source</a>
</small>
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.7">0.0.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-top-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L155" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L158" target="_blank">View source</a>
</small>
@@ -135,7 +135,7 @@ def f(x: object):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L206" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L209" target="_blank">View source</a>
</small>
@@ -167,7 +167,7 @@ f(int) # error
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L232" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L235" target="_blank">View source</a>
</small>
@@ -198,7 +198,7 @@ a = 1
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L257" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260" target="_blank">View source</a>
</small>
@@ -230,7 +230,7 @@ class C(A, B): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L283" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286" target="_blank">View source</a>
</small>
@@ -262,7 +262,7 @@ class B(A): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L309" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L312" target="_blank">View source</a>
</small>
@@ -290,7 +290,7 @@ type B = A
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L353" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L356" target="_blank">View source</a>
</small>
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L331" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L334" target="_blank">View source</a>
</small>
@@ -346,7 +346,7 @@ false positives it can produce.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L374" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L377" target="_blank">View source</a>
</small>
@@ -373,7 +373,7 @@ class B(A, A): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L398" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L621" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L624" target="_blank">View source</a>
</small>
@@ -559,7 +559,7 @@ class C(A, B): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L648" target="_blank">View source</a>
</small>
@@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430" target="_blank">View source</a>
</small>
@@ -674,7 +674,7 @@ an atypical memory layout.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L699" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702" target="_blank">View source</a>
</small>
@@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L742" target="_blank">View source</a>
</small>
@@ -729,7 +729,7 @@ a: int = ''
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2042" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2085" target="_blank">View source</a>
</small>
@@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L764" target="_blank">View source</a>
</small>
@@ -799,7 +799,7 @@ asyncio.run(main())
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L791" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L794" target="_blank">View source</a>
</small>
@@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L842" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L845" target="_blank">View source</a>
</small>
@@ -850,7 +850,7 @@ with 1:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L863" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L866" target="_blank">View source</a>
</small>
@@ -879,7 +879,7 @@ a: str
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L889" target="_blank">View source</a>
</small>
@@ -923,7 +923,7 @@ except ZeroDivisionError:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1755" target="_blank">View source</a>
</small>
@@ -965,7 +965,7 @@ class D(A):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.35">0.0.1-alpha.35</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2268" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2336" target="_blank">View source</a>
</small>
@@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L925" target="_blank">View source</a>
</small>
@@ -1077,7 +1077,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L669" target="_blank">View source</a>
</small>
@@ -1116,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L953" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956" target="_blank">View source</a>
</small>
@@ -1151,7 +1151,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1050" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1053" target="_blank">View source</a>
</small>
@@ -1185,7 +1185,7 @@ class B(metaclass=f): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2170" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2238" target="_blank">View source</a>
</small>
@@ -1292,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L576" target="_blank">View source</a>
</small>
@@ -1346,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1026" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1029" target="_blank">View source</a>
</small>
@@ -1376,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1077" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1080" target="_blank">View source</a>
</small>
@@ -1426,7 +1426,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1179" target="_blank">View source</a>
</small>
@@ -1452,7 +1452,7 @@ def f(a: int = ''): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L984" target="_blank">View source</a>
</small>
@@ -1483,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L509" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L512" target="_blank">View source</a>
</small>
@@ -1517,7 +1517,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1199" target="_blank">View source</a>
</small>
@@ -1566,7 +1566,7 @@ def g():
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L720" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L723" target="_blank">View source</a>
</small>
@@ -1591,7 +1591,7 @@ def func() -> int:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1239" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242" target="_blank">View source</a>
</small>
@@ -1681,13 +1681,59 @@ class C: ...
- [Typing spec: The meaning of annotations](https://typing.python.org/en/latest/spec/annotations.html#the-meaning-of-annotations)
- [Typing spec: String annotations](https://typing.python.org/en/latest/spec/annotations.html#string-annotations)
## `invalid-total-ordering`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.10">0.0.10</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-total-ordering" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2374" target="_blank">View source</a>
</small>
**What it does**
Checks for classes decorated with `@functools.total_ordering` that don't
define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).
**Why is this bad?**
The `@total_ordering` decorator requires the class to define at least one
ordering method. If none is defined, Python raises a `ValueError` at runtime.
**Example**
```python
from functools import total_ordering
@total_ordering
class MyClass: # Error: no ordering method defined
def __eq__(self, other: object) -> bool:
return True
```
Use instead:
```python
from functools import total_ordering
@total_ordering
class MyClass:
def __eq__(self, other: object) -> bool:
return True
def __lt__(self, other: "MyClass") -> bool:
return True
```
## `invalid-type-alias-type`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1005" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1008" target="_blank">View source</a>
</small>
@@ -1714,7 +1760,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1474" target="_blank">View source</a>
</small>
@@ -1761,7 +1807,7 @@ Bar[int] # error: too few arguments
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1278" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1281" target="_blank">View source</a>
</small>
@@ -1791,7 +1837,7 @@ TYPE_CHECKING = ''
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1305" target="_blank">View source</a>
</small>
@@ -1821,7 +1867,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1354" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1357" target="_blank">View source</a>
</small>
@@ -1855,7 +1901,7 @@ f(10) # Error
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1326" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1329" target="_blank">View source</a>
</small>
@@ -1889,7 +1935,7 @@ class C:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1382" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1385" target="_blank">View source</a>
</small>
@@ -1918,13 +1964,44 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar
## `invalid-typed-dict-statement`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.9">0.0.9</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-typed-dict-statement" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2213" target="_blank">View source</a>
</small>
**What it does**
Detects statements other than annotated declarations in `TypedDict` class bodies.
**Why is this bad?**
`TypedDict` class bodies aren't allowed to contain any other types of statements. For
example, method definitions and field values aren't allowed. None of these will be
available on "instances of the `TypedDict`" at runtime (as `dict` is the runtime class of
all "`TypedDict` instances").
**Example**
```python
from typing import TypedDict
class Foo(TypedDict):
def bar(self): # error: [invalid-typed-dict-statement]
pass
```
## `missing-argument`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1414" target="_blank">View source</a>
</small>
@@ -1949,7 +2026,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2143" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2186" target="_blank">View source</a>
</small>
@@ -1982,7 +2059,7 @@ alice["age"] # KeyError
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1433" target="_blank">View source</a>
</small>
@@ -2011,7 +2088,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1515" target="_blank">View source</a>
</small>
@@ -2037,7 +2114,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1456" target="_blank">View source</a>
</small>
@@ -2061,7 +2138,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1685" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1728" target="_blank">View source</a>
</small>
@@ -2094,7 +2171,7 @@ class B(A):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1566" target="_blank">View source</a>
</small>
@@ -2121,7 +2198,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1896" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1939" target="_blank">View source</a>
</small>
@@ -2148,7 +2225,7 @@ f(x=1) # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1584" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1587" target="_blank">View source</a>
</small>
@@ -2176,7 +2253,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L180" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183" target="_blank">View source</a>
</small>
@@ -2208,7 +2285,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1606" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1609" target="_blank">View source</a>
</small>
@@ -2245,7 +2322,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1636" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1639" target="_blank">View source</a>
</small>
@@ -2309,7 +2386,7 @@ def test(): -> "int":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2070" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2113" target="_blank">View source</a>
</small>
@@ -2336,7 +2413,7 @@ cast(int, f()) # Redundant
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2018" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2061" target="_blank">View source</a>
</small>
@@ -2360,13 +2437,59 @@ static_assert(1 + 1 == 3) # error: evaluates to `False`
static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness
```
## `subclass-of-dataclass-with-order`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.11">0.0.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-dataclass-with-order" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1688" target="_blank">View source</a>
</small>
**What it does**
Checks for classes that inherit from a dataclass with `order=True`.
**Why is this bad?**
When a dataclass has `order=True`, comparison methods (`__lt__`, `__le__`, `__gt__`, `__ge__`)
are generated that compare instances as tuples of their fields. These methods raise a
`TypeError` at runtime when comparing instances of different classes in the inheritance
hierarchy, even if one is a subclass of the other.
This violates the [Liskov Substitution Principle] because child class instances cannot be
used in all contexts where parent class instances are expected.
**Example**
```python
from dataclasses import dataclass
@dataclass(order=True)
class Parent:
value: int
class Child(Parent): # Ty emits a warning here
pass
# At runtime, this raises TypeError:
# Child(1) < Parent(2)
```
Consider using [`functools.total_ordering`] instead, which does not have this limitation.
[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
[`functools.total_ordering`]: https://docs.python.org/3/library/functools.html#functools.total_ordering
## `subclass-of-final-class`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1665" target="_blank">View source</a>
</small>
@@ -2395,7 +2518,7 @@ class B(A): ... # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1830" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1873" target="_blank">View source</a>
</small>
@@ -2429,7 +2552,7 @@ class F(NamedTuple):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1770" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1813" target="_blank">View source</a>
</small>
@@ -2456,7 +2579,7 @@ f("foo") # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1748" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
</small>
@@ -2484,7 +2607,7 @@ def _(x: int):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1834" target="_blank">View source</a>
</small>
@@ -2530,7 +2653,7 @@ class A:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1900" target="_blank">View source</a>
</small>
@@ -2554,7 +2677,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1875" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1918" target="_blank">View source</a>
</small>
@@ -2581,7 +2704,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1917" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1960" target="_blank">View source</a>
</small>
@@ -2609,7 +2732,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2091" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2134" target="_blank">View source</a>
</small>
@@ -2667,7 +2790,7 @@ def g():
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1939" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1982" target="_blank">View source</a>
</small>
@@ -2692,7 +2815,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1958" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2001" target="_blank">View source</a>
</small>
@@ -2717,7 +2840,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812" target="_blank">View source</a>
</small>
@@ -2756,7 +2879,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1532" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1535" target="_blank">View source</a>
</small>
@@ -2793,7 +2916,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1977" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2020" target="_blank">View source</a>
</small>
@@ -2852,7 +2975,7 @@ a = 20 / 2
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1120" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1123" target="_blank">View source</a>
</small>
@@ -2915,7 +3038,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1999" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2042" target="_blank">View source</a>
</small>

View File

@@ -3,6 +3,7 @@ auto-import-includes-modules,main.py,0,1
auto-import-includes-modules,main.py,1,7
auto-import-includes-modules,main.py,2,1
auto-import-skips-current-module,main.py,0,1
class-arg-completion,main.py,0,1
fstring-completions,main.py,0,1
higher-level-symbols-preferred,main.py,0,
higher-level-symbols-preferred,main.py,1,1
1 name file index rank
3 auto-import-includes-modules main.py 1 7
4 auto-import-includes-modules main.py 2 1
5 auto-import-skips-current-module main.py 0 1
6 class-arg-completion main.py 0 1
7 fstring-completions main.py 0 1
8 higher-level-symbols-preferred main.py 0
9 higher-level-symbols-preferred main.py 1 1

View File

@@ -0,0 +1,2 @@
[settings]
auto-import = false

View File

@@ -0,0 +1 @@
class Foo(m<CURSOR: metaclass>)

View File

@@ -0,0 +1,5 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []

View File

@@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }

View File

@@ -193,15 +193,16 @@ impl<'db> Completions<'db> {
/// when the completion context determines that the given suggestion
/// is never valid.
fn add_skip_query(&mut self, mut completion: Completion<'db>) -> bool {
// Tags completions with whether they are known to be usable in
// a `raise` context.
// Tags completions with context-specific if they are
// known to be usable in a `raise` context and we have
// determined a raisable type `raisable_ty`.
//
// It's possible that some completions are usable in a `raise`
// but aren't marked here. That is, false negatives are
// possible but false positives are not.
if let Some(raisable_ty) = self.context.raisable_ty {
if let Some(ty) = completion.ty {
completion.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_ty);
completion.is_context_specific |= ty.is_assignable_to(self.db, raisable_ty);
}
}
if self.context.exclude(self.db, &completion) {
@@ -285,13 +286,13 @@ pub struct Completion<'db> {
/// Whether this item only exists for type checking purposes and
/// will be missing at runtime
pub is_type_check_only: bool,
/// Whether this item can definitively be used in a `raise` context.
/// Whether this item can definitively be used in the current context.
///
/// Note that this may not always be computed. (i.e., Only computed
/// when we are in a `raise` context.) And also note that if this
/// is `true`, then it's definitively usable in `raise`, but if
/// it's `false`, it _may_ still be usable in `raise`.
pub is_definitively_raisable: bool,
/// Some completions are computed based on contextual information.
/// If that's the case, we know this is a very precise completion
/// that should always be valid and can be preferred when
/// ordering completions.
pub is_context_specific: bool,
/// The documentation associated with this item, if
/// available.
pub documentation: Option<Docstring>,
@@ -315,7 +316,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: semantic.builtin,
is_type_check_only,
is_definitively_raisable: false,
is_context_specific: false,
documentation,
}
}
@@ -398,7 +399,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: false,
is_type_check_only: false,
is_definitively_raisable: false,
is_context_specific: false,
documentation: None,
}
}
@@ -414,7 +415,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: true,
is_type_check_only: false,
is_definitively_raisable: false,
is_context_specific: false,
documentation: None,
}
}
@@ -433,7 +434,7 @@ impl<'db> Completion<'db> {
import: None,
builtin: false,
is_type_check_only: false,
is_definitively_raisable: false,
is_context_specific: true,
documentation,
}
}
@@ -994,7 +995,7 @@ impl<'db> CollectionContext<'db> {
#[allow(clippy::unused_self)]
fn rank<'c>(&self, c: &'c Completion<'_>) -> Rank<'c> {
Rank {
definitively_usable: if c.is_definitively_raisable {
definitively_usable: if c.is_context_specific {
Sort::Higher
} else {
Sort::Even
@@ -1183,7 +1184,6 @@ fn add_function_arg_completions<'db>(
if p.is_positional_only || set_function_args.contains(&p.name.as_str()) {
continue;
}
completions.add(Completion::argument(
&p.name,
p.ty,
@@ -1374,7 +1374,7 @@ fn add_unimported_completions<'db>(
builtin: false,
// TODO: `is_type_check_only` requires inferring the type of the symbol
is_type_check_only: false,
is_definitively_raisable: false,
is_context_specific: false,
documentation: None,
});
}
@@ -3088,9 +3088,9 @@ class Foo(<CURSOR>):
);
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
metaclass=
Bar
Foo
metaclass=
");
}
@@ -3106,9 +3106,9 @@ class Bar: ...
);
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
metaclass=
Bar
Foo
metaclass=
");
}
@@ -3124,9 +3124,9 @@ class Bar: ...
);
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
metaclass=
Bar
Foo
metaclass=
");
}
@@ -3140,9 +3140,9 @@ class Foo(<CURSOR>",
);
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
metaclass=
Bar
Foo
metaclass=
");
}
@@ -3804,8 +3804,8 @@ bar(o<CURSOR>
assert_snapshot!(
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
@"
foo
okay=
foo
"
);
}
@@ -3825,8 +3825,8 @@ bar(o<CURSOR>
assert_snapshot!(
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
@"
foo
okay=
foo
"
);
}
@@ -3940,10 +3940,10 @@ bar(o<CURSOR>
assert_snapshot!(
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
@"
foo
okay=
okay_abc=
okay_okay=
foo
"
);
}
@@ -3961,9 +3961,9 @@ bar(<CURSOR>
);
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
okay=
bar
foo
okay=
");
}

View File

@@ -218,6 +218,7 @@ fn render_markdown(docstring: &str) -> String {
output.push('\n');
}
}
first_line = false;
// If we're in a literal block and we find a non-empty dedented line, end the block
// TODO: we should remove all the trailing blank lines
@@ -273,6 +274,22 @@ fn render_markdown(docstring: &str) -> String {
block_indent = line_indent;
in_any_code = true;
in_markdown_with_fence = Some(fence.to_owned());
// Render the line verbatim without its indent and move on.
//
// If there's any indent this is really just Bad Syntax but it "makes sense"
// to someone writing docs like this:
//
// Returns:
// Some details...
// ```
// some_example()
// ```
// etc etc...
//
// We "make this work" by stripping the indent on the fences but preserving the
// full indent of the lines between the fences
output.push_str(line);
continue;
}
// If we're in a markdown code fence and this line seems to terminate it, end the block
} else if let Some(fence) = &in_markdown_with_fence
@@ -281,6 +298,9 @@ fn render_markdown(docstring: &str) -> String {
in_any_code = false;
block_indent = 0;
in_markdown_with_fence = None;
// Render the line without its indent and move on.
output.push_str(line);
continue;
}
// If we're not in a codeblock and we see something that signals a literal block, start one
@@ -446,8 +466,6 @@ fn render_markdown(docstring: &str) -> String {
// Print the line verbatim, it's in code
output.push_str(line);
}
first_line = false;
}
// Flush codeblock
if in_any_code {
@@ -1208,6 +1226,74 @@ mod tests {
");
}
// If an explicit markdown codefence is indented, eat the indent so it renders
// "the way the user expects" (as written this is basically invalid markdown,
// but it's nice if we handle it anyway because it makes visual sense).
#[test]
fn explicit_markdown_block_with_indent_tick() {
let docstring = r#"
My cool func...
Returns:
Some details
`````python
x_y = thing_do();
``` # this should't close the fence!
a_b = other_thing();
`````
And so on.
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
My cool func...
Returns:
&nbsp;&nbsp;&nbsp;&nbsp;Some details
`````python
x_y = thing_do();
``` # this should't close the fence!
a_b = other_thing();
`````
&nbsp;&nbsp;&nbsp;&nbsp;And so on.
");
}
// If an explicit markdown codefence is indented, eat the indent so it renders
// "the way the user expects" (as written this is basically invalid markdown,
// but it's nice if we handle it anyway because it makes visual sense).
#[test]
fn explicit_markdown_block_with_indent_tilde() {
let docstring = r#"
My cool func...
Returns:
Some details
~~~~~~python
x_y = thing_do();
~~~ # this should't close the fence!
a_b = other_thing();
~~~~~~
And so on.
"#;
let docstring = Docstring::new(docstring.to_owned());
assert_snapshot!(docstring.render_markdown(), @r"
My cool func...
Returns:
&nbsp;&nbsp;&nbsp;&nbsp;Some details
~~~~~~python
x_y = thing_do();
~~~ # this should't close the fence!
a_b = other_thing();
~~~~~~
&nbsp;&nbsp;&nbsp;&nbsp;And so on.
");
}
// What do we do when we hit the end of the docstring with an unclosed markdown block?
#[test]
fn explicit_markdown_block_with_unclosed_fence_tick() {
@@ -1267,7 +1353,7 @@ mod tests {
assert_snapshot!(docstring.render_markdown(), @r"
My cool func:
``````we still think this is a codefence```
``````we still think this is a codefence```
x_y = thing_do();
```````````` and are sloppy as heck with indentation and closing shrugggg
");
@@ -1290,7 +1376,7 @@ mod tests {
assert_snapshot!(docstring.render_markdown(), @r"
My cool func:
~~~~~~we still think this is a codefence~~~
~~~~~~we still think this is a codefence~~~
x_y = thing_do();
~~~~~~~~~~~~~ and are sloppy as heck with indentation and closing shrugggg
");

View File

@@ -619,7 +619,7 @@ mod tests {
list_snapshot(&db),
@r#"
[
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
]
"#,
);
@@ -662,7 +662,7 @@ mod tests {
@r#"
[
Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
Module::File("random", "std-custom", "/typeshed/stdlib/random.pyi", Module, None),
]
"#,
@@ -755,7 +755,7 @@ mod tests {
[
Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
Module::File("collections", "std-custom", "/typeshed/stdlib/collections/__init__.pyi", Package, Some(Collections)),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
]
"#,
);
@@ -1091,7 +1091,7 @@ mod tests {
list_snapshot(&db),
@r#"
[
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
]
"#,
);
@@ -1107,7 +1107,7 @@ mod tests {
list_snapshot(&db),
@r#"
[
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
]
"#,
);
@@ -1129,7 +1129,7 @@ mod tests {
list_snapshot(&db),
@r#"
[
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
]
"#,
);
@@ -1191,7 +1191,7 @@ mod tests {
list_snapshot(&db),
@r#"
[
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
]
"#,
);

View File

@@ -320,6 +320,7 @@ pub enum KnownModule {
Abc,
Contextlib,
Dataclasses,
Functools,
Collections,
Inspect,
#[strum(serialize = "string.templatelib")]
@@ -351,6 +352,7 @@ impl KnownModule {
Self::Abc => "abc",
Self::Contextlib => "contextlib",
Self::Dataclasses => "dataclasses",
Self::Functools => "functools",
Self::Collections => "collections",
Self::Inspect => "inspect",
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
@@ -395,6 +397,10 @@ impl KnownModule {
pub const fn is_importlib(self) -> bool {
matches!(self, Self::ImportLib)
}
pub const fn is_functools(self) -> bool {
matches!(self, Self::Functools)
}
}
impl std::fmt::Display for KnownModule {

View File

@@ -349,6 +349,83 @@ GenericWithOrder[int](1) < GenericWithOrder[int](1)
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
```
Subclassing a dataclass with `order=True` is problematic because comparing instances of different
classes in the inheritance hierarchy will raise a `TypeError` at runtime. The design of the stdlib
feature therefore violates the Liskov Substitution Principle:
```py
from dataclasses import dataclass
@dataclass(order=True)
class Parent:
value: int
class Child(Parent): # error: [subclass-of-dataclass-with-order]
pass
# The comparison methods generated by @dataclass(order=True) compare instances
# as tuples of their fields. At runtime, this raises TypeError when comparing
# instances of different classes in the hierarchy:
# Child(42) < Parent(42) # TypeError!
```
This also applies when the child class is also a dataclass:
```py
@dataclass(order=True)
class OrderedParent:
x: int
@dataclass
class OrderedChild(OrderedParent): # error: [subclass-of-dataclass-with-order]
y: str
```
If the child class also has `order=True`, the diagnostic is suppressed because the child overrides
all comparison methods:
```py
@dataclass(order=True)
class OrderedParent2:
x: int
@dataclass(order=True)
class OrderedChild2(OrderedParent2):
y: str
```
If the child class manually overrides all comparison methods, the diagnostic is also suppressed:
```py
@dataclass(order=True)
class OrderedParent3:
x: int
class ManualChild(OrderedParent3): # No warning - all comparison methods overridden
def __lt__(self, other: OrderedParent3) -> bool:
return True
def __le__(self, other: OrderedParent3) -> bool:
return True
def __gt__(self, other: OrderedParent3) -> bool:
return True
def __ge__(self, other: OrderedParent3) -> bool:
return True
```
If the parent dataclass does not have `order=True`, no warning is emitted:
```py
@dataclass
class UnorderedParent:
x: int
class UnorderedChild(UnorderedParent): # No warning
pass
```
If a class already defines one of the comparison methods, a `TypeError` is raised at runtime.
Ideally, we would emit a diagnostic in that case:
@@ -583,7 +660,7 @@ from module import NotFrozenBase
@final
@dataclass(frozen=True)
@total_ordering
@total_ordering # error: [invalid-total-ordering]
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
y: str
```

View File

@@ -23,17 +23,6 @@ alice.role = "moderator"
bob = Member(name="Bob", tag="VIP")
```
## With `Literal` types
```py
from typing import Literal
import dataclasses
@dataclasses.dataclass
class Foo:
x: Literal["f"] = dataclasses.field(init=False, default="f")
```
## `default_factory`
The `default_factory` argument can be used to specify a callable that provides a default value for a

View File

@@ -0,0 +1,246 @@
# `functools.total_ordering`
The `@functools.total_ordering` decorator allows a class to define a single comparison method (like
`__lt__`), and the decorator automatically generates the remaining comparison methods (`__le__`,
`__gt__`, `__ge__`). Defining `__eq__` is optional, as it can be inherited from `object`.
## Basic usage
When a class defines `__eq__` and `__lt__`, the decorator synthesizes `__le__`, `__gt__`, and
`__ge__`:
```py
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, grade: int):
self.grade = grade
def __eq__(self, other: object) -> bool:
if not isinstance(other, Student):
return NotImplemented
return self.grade == other.grade
def __lt__(self, other: "Student") -> bool:
return self.grade < other.grade
s1 = Student(85)
s2 = Student(90)
# User-defined comparison methods work as expected.
reveal_type(s1 == s2) # revealed: bool
reveal_type(s1 < s2) # revealed: bool
# Synthesized comparison methods are available.
reveal_type(s1 <= s2) # revealed: bool
reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Using `__gt__` as the root comparison method
When a class defines `__eq__` and `__gt__`, the decorator synthesizes `__lt__`, `__le__`, and
`__ge__`:
```py
from functools import total_ordering
@total_ordering
class Priority:
def __init__(self, level: int):
self.level = level
def __eq__(self, other: object) -> bool:
if not isinstance(other, Priority):
return NotImplemented
return self.level == other.level
def __gt__(self, other: "Priority") -> bool:
return self.level > other.level
p1 = Priority(1)
p2 = Priority(2)
# User-defined comparison methods work
reveal_type(p1 == p2) # revealed: bool
reveal_type(p1 > p2) # revealed: bool
# Synthesized comparison methods are available
reveal_type(p1 < p2) # revealed: bool
reveal_type(p1 <= p2) # revealed: bool
reveal_type(p1 >= p2) # revealed: bool
```
## Inherited `__eq__`
A class only needs to define a single comparison method. The `__eq__` method can be inherited from
`object`:
```py
from functools import total_ordering
@total_ordering
class Score:
def __init__(self, value: int):
self.value = value
def __lt__(self, other: "Score") -> bool:
return self.value < other.value
s1 = Score(85)
s2 = Score(90)
# `__eq__` is inherited from object.
reveal_type(s1 == s2) # revealed: bool
# Synthesized comparison methods are available.
reveal_type(s1 <= s2) # revealed: bool
reveal_type(s1 > s2) # revealed: bool
reveal_type(s1 >= s2) # revealed: bool
```
## Inherited ordering methods
The decorator also works when the ordering method is inherited from a superclass:
```py
from functools import total_ordering
class Base:
def __lt__(self, other: "Base") -> bool:
return True
@total_ordering
class Child(Base):
def __eq__(self, other: object) -> bool:
if not isinstance(other, Child):
return NotImplemented
return True
c1 = Child()
c2 = Child()
# Synthesized methods work even though `__lt__` is inherited.
reveal_type(c1 <= c2) # revealed: bool
reveal_type(c1 > c2) # revealed: bool
reveal_type(c1 >= c2) # revealed: bool
```
## Explicitly-defined methods are not overridden
When a class explicitly defines multiple comparison methods, the decorator does not override them.
We use a narrower return type (`Literal[True]`) to verify that the explicit methods are preserved:
```py
from functools import total_ordering
from typing import Literal
@total_ordering
class Temperature:
def __init__(self, celsius: float):
self.celsius = celsius
def __lt__(self, other: "Temperature") -> Literal[True]:
return True
def __gt__(self, other: "Temperature") -> Literal[True]:
return True
t1 = Temperature(20.0)
t2 = Temperature(25.0)
# User-defined methods preserve their return type.
reveal_type(t1 < t2) # revealed: Literal[True]
reveal_type(t1 > t2) # revealed: Literal[True]
# Synthesized methods have `bool` return type.
reveal_type(t1 <= t2) # revealed: bool
reveal_type(t1 >= t2) # revealed: bool
```
## Combined with `@dataclass`
The decorator works with `@dataclass`:
```py
from dataclasses import dataclass
from functools import total_ordering
@total_ordering
@dataclass
class Point:
x: int
y: int
def __lt__(self, other: "Point") -> bool:
return (self.x, self.y) < (other.x, other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
# Dataclass-synthesized `__eq__` is available.
reveal_type(p1 == p2) # revealed: bool
# User-defined comparison method works.
reveal_type(p1 < p2) # revealed: bool
# Synthesized comparison methods are available.
reveal_type(p1 <= p2) # revealed: bool
reveal_type(p1 > p2) # revealed: bool
reveal_type(p1 >= p2) # revealed: bool
```
## Missing ordering method
If a class has `@total_ordering` but doesn't define any ordering method (itself or in a superclass),
a diagnostic is emitted at the decorator site:
```py
from functools import total_ordering
@total_ordering # error: [invalid-total-ordering]
class NoOrdering:
def __eq__(self, other: object) -> bool:
return True
n1 = NoOrdering()
n2 = NoOrdering()
# Comparison operators also error because no methods were synthesized.
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```
## Without the decorator
Without `@total_ordering`, classes that only define `__lt__` will not have `__le__` or `__ge__`
synthesized:
```py
class NoDecorator:
def __init__(self, value: int):
self.value = value
def __eq__(self, other: object) -> bool:
if not isinstance(other, NoDecorator):
return NotImplemented
return self.value == other.value
def __lt__(self, other: "NoDecorator") -> bool:
return self.value < other.value
n1 = NoDecorator(1)
n2 = NoDecorator(2)
# User-defined methods work.
reveal_type(n1 == n2) # revealed: bool
reveal_type(n1 < n2) # revealed: bool
# Note: `n1 > n2` works because Python reflects it to `n2 < n1`
reveal_type(n1 > n2) # revealed: bool
# These comparison operators are not available.
n1 <= n2 # error: [unsupported-operator]
n1 >= n2 # error: [unsupported-operator]
```

View File

@@ -475,37 +475,26 @@ from typing import NamedTuple
class Foo:
x: int
class Bar(Foo):
class Bar(Foo): # error: [subclass-of-dataclass-with-order]
def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
# TODO: specifying `order=True` on the subclass means that a `__lt__` method is
# generated that is incompatible with the generated `__lt__` method on the superclass.
# We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
# be `invalid-method-override` since we'd emit it on the class definition rather than
# on any method definition. Note also that no other type checker complains about this
# as of 2025-11-21.
# Specifying `order=True` on the subclass means that a `__lt__` method is generated that
# is incompatible with the generated `__lt__` method on the superclass. We don't emit the
# `subclass-of-dataclass-with-order` diagnostic here because the child class overrides all
# comparison methods.
# TODO: We should also emit `invalid-method-override` diagnostics for each generated
# comparison method since they have incompatible signatures.
@dataclass(order=True)
class Bar2(Foo):
y: str
# TODO: Although this class does not override any methods of `Foo`, the design of the
# `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
# Although this class does not override any methods of `Foo`, the design of the
# `order=True` stdlib dataclasses feature itself violates the Liskov Substitution
# Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
# expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
# and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
# be compared with instances of subclasses of `Foo`).
#
# Many users would probably like their type checkers to alert them to cases where instances
# of subclasses cannot be substituted for instances of superclasses, as this violates many
# assumptions a type checker will make and makes it likely that a type checker will fail to
# catch type errors elsewhere in the user's code. We could therefore consider treating all
# `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
# this probably shouldn't be reported with the same error code as Liskov violations, since
# the error does not stem from any method signatures written by the user. The example is
# only included here for completeness.
#
# Note that no other type checker catches this error as of 2025-11-21.
class Bar3(Foo): ...
class Bar3(Foo): ... # error: [subclass-of-dataclass-with-order]
class Eggs:
def __lt__(self, other: Eggs) -> bool: ...

View File

@@ -530,7 +530,7 @@ from dataclasses import dataclass
class ParentDataclass:
x: int
class Child(ParentDataclass):
class Child(ParentDataclass): # error: [subclass-of-dataclass-with-order]
@override
def __lt__(self, other: ParentDataclass) -> bool: ... # fine
@@ -551,10 +551,8 @@ class MyNamedTupleChild(MyNamedTupleParent):
class MyTypedDict(TypedDict):
x: int
# error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
@override
# TODO: it's invalid to define a method on a `TypedDict` class,
# so we should emit a diagnostic here.
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
def copy(self) -> Self: ...
class Grandparent(Any): ...

View File

@@ -61,7 +61,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
6 |
7 | @final
8 | @dataclass(frozen=True)
9 | @total_ordering
9 | @total_ordering # error: [invalid-total-ordering]
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
```
@@ -126,6 +126,22 @@ info: rule `invalid-frozen-dataclass-subclass` is enabled by default
```
```
error[invalid-total-ordering]: Class decorated with `@total_ordering` must define at least one ordering method
--> src/main.py:9:1
|
7 | @final
8 | @dataclass(frozen=True)
9 | @total_ordering # error: [invalid-total-ordering]
| ^^^^^^^^^^^^^^^ `FrozenChild` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
|
info: The decorator will raise `ValueError` at runtime
info: rule `invalid-total-ordering` is enabled by default
```
```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/main.py:8:1
@@ -133,7 +149,7 @@ error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from n
7 | @final
8 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
9 | @total_ordering
9 | @total_ordering # error: [invalid-total-ordering]
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
| ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
11 | y: str

View File

@@ -1,5 +1,6 @@
---
source: crates/ty_test/src/lib.rs
assertion_line: 623
expression: snapshot
---
@@ -20,66 +21,70 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
5 | class Foo:
6 | x: int
7 |
8 | class Bar(Foo):
8 | class Bar(Foo): # error: [subclass-of-dataclass-with-order]
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
10 |
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
12 | # generated that is incompatible with the generated `__lt__` method on the superclass.
13 | # We could consider detecting this and emitting a diagnostic, though maybe it shouldn't
14 | # be `invalid-method-override` since we'd emit it on the class definition rather than
15 | # on any method definition. Note also that no other type checker complains about this
16 | # as of 2025-11-21.
11 | # Specifying `order=True` on the subclass means that a `__lt__` method is generated that
12 | # is incompatible with the generated `__lt__` method on the superclass. We don't emit the
13 | # `subclass-of-dataclass-with-order` diagnostic here because the child class overrides all
14 | # comparison methods.
15 | # TODO: We should also emit `invalid-method-override` diagnostics for each generated
16 | # comparison method since they have incompatible signatures.
17 | @dataclass(order=True)
18 | class Bar2(Foo):
19 | y: str
20 |
21 | # TODO: Although this class does not override any methods of `Foo`, the design of the
22 | # `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
21 | # Although this class does not override any methods of `Foo`, the design of the
22 | # `order=True` stdlib dataclasses feature itself violates the Liskov Substitution
23 | # Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
24 | # expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
25 | # and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
26 | # be compared with instances of subclasses of `Foo`).
27 | #
28 | # Many users would probably like their type checkers to alert them to cases where instances
29 | # of subclasses cannot be substituted for instances of superclasses, as this violates many
30 | # assumptions a type checker will make and makes it likely that a type checker will fail to
31 | # catch type errors elsewhere in the user's code. We could therefore consider treating all
32 | # `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
33 | # this probably shouldn't be reported with the same error code as Liskov violations, since
34 | # the error does not stem from any method signatures written by the user. The example is
35 | # only included here for completeness.
36 | #
37 | # Note that no other type checker catches this error as of 2025-11-21.
38 | class Bar3(Foo): ...
39 |
40 | class Eggs:
41 | def __lt__(self, other: Eggs) -> bool: ...
42 |
43 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
44 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
45 | # diagnostic here but pyright and pyrefly do not.
46 | @dataclass(order=True)
47 | class Ham(Eggs):
48 | x: int
49 |
50 | class Baz(NamedTuple):
51 | x: int
52 |
53 | class Spam(Baz):
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
27 | class Bar3(Foo): ... # error: [subclass-of-dataclass-with-order]
28 |
29 | class Eggs:
30 | def __lt__(self, other: Eggs) -> bool: ...
31 |
32 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
33 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
34 | # diagnostic here but pyright and pyrefly do not.
35 | @dataclass(order=True)
36 | class Ham(Eggs):
37 | x: int
38 |
39 | class Baz(NamedTuple):
40 | x: int
41 |
42 | class Spam(Baz):
43 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
```
# Diagnostics
```
warning[subclass-of-dataclass-with-order]: Class `Bar` inherits from dataclass `Foo` which has `order=True`
--> src/mdtest_snippet.pyi:8:11
|
6 | x: int
7 |
8 | class Bar(Foo): # error: [subclass-of-dataclass-with-order]
| ^^^
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
|
info: Comparison of instances of the child class with instances of the parent class will raise `TypeError` at runtime
info: rule `subclass-of-dataclass-with-order` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `__lt__`
--> src/mdtest_snippet.pyi:9:9
|
8 | class Bar(Foo):
8 | class Bar(Foo): # error: [subclass-of-dataclass-with-order]
9 | def __lt__(self, other: Bar) -> bool: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Foo.__lt__`
10 |
11 | # TODO: specifying `order=True` on the subclass means that a `__lt__` method is
11 | # Specifying `order=True` on the subclass means that a `__lt__` method is generated that
|
info: This violates the Liskov Substitution Principle
info: `Foo.__lt__` is a generated method created because `Foo` is a dataclass
@@ -95,22 +100,38 @@ info: rule `invalid-method-override` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `_asdict`
--> src/mdtest_snippet.pyi:54:9
warning[subclass-of-dataclass-with-order]: Class `Bar3` inherits from dataclass `Foo` which has `order=True`
--> src/mdtest_snippet.pyi:27:12
|
53 | class Spam(Baz):
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
25 | # and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
26 | # be compared with instances of subclasses of `Foo`).
27 | class Bar3(Foo): ... # error: [subclass-of-dataclass-with-order]
| ^^^
28 |
29 | class Eggs:
|
info: Comparison of instances of the child class with instances of the parent class will raise `TypeError` at runtime
info: rule `subclass-of-dataclass-with-order` is enabled by default
```
```
error[invalid-method-override]: Invalid override of method `_asdict`
--> src/mdtest_snippet.pyi:43:9
|
42 | class Spam(Baz):
43 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict`
|
info: This violates the Liskov Substitution Principle
info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple`
--> src/mdtest_snippet.pyi:50:7
--> src/mdtest_snippet.pyi:39:7
|
48 | x: int
49 |
50 | class Baz(NamedTuple):
37 | x: int
38 |
39 | class Baz(NamedTuple):
| ^^^^^^^^^^^^^^^ Definition of `Baz`
51 | x: int
40 | x: int
|
info: rule `invalid-method-override` is enabled by default

View File

@@ -0,0 +1,103 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: typed_dict.md - `TypedDict` - Only annotated declarations are allowed in the class body
mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypedDict
2 |
3 | class Foo(TypedDict):
4 | """docstring"""
5 |
6 | annotated_item: int
7 | """attribute docstring"""
8 |
9 | pass
10 |
11 | # As a non-standard but common extension, we interpret `...` as equivalent to `pass`.
12 | ...
13 |
14 | class Bar(TypedDict):
15 | a: int
16 | # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
17 | 42
18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
19 | b: str = "hello"
20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
21 | def bar(self): ...
22 | class Baz(Bar):
23 | # error: [invalid-typed-dict-statement]
24 | def baz(self):
25 | pass
```
# Diagnostics
```
error[invalid-typed-dict-statement]: invalid statement in TypedDict class body
--> src/mdtest_snippet.py:17:5
|
15 | a: int
16 | # error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
17 | 42
| ^^
18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
19 | b: str = "hello"
|
info: Only annotated declarations (`<name>: <type>`) are allowed.
info: rule `invalid-typed-dict-statement` is enabled by default
```
```
error[invalid-typed-dict-statement]: TypedDict item cannot have a value
--> src/mdtest_snippet.py:19:14
|
17 | 42
18 | # error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
19 | b: str = "hello"
| ^^^^^^^
20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
21 | def bar(self): ...
|
info: rule `invalid-typed-dict-statement` is enabled by default
```
```
error[invalid-typed-dict-statement]: TypedDict class cannot have methods
--> src/mdtest_snippet.py:21:5
|
19 | b: str = "hello"
20 | # error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
21 | def bar(self): ...
| ^^^^^^^^^^^^^^^^^^
22 | class Baz(Bar):
23 | # error: [invalid-typed-dict-statement]
|
info: rule `invalid-typed-dict-statement` is enabled by default
```
```
error[invalid-typed-dict-statement]: TypedDict class cannot have methods
--> src/mdtest_snippet.py:24:5
|
22 | class Baz(Bar):
23 | # error: [invalid-typed-dict-statement]
24 | / def baz(self):
25 | | pass
| |____________^
|
info: rule `invalid-typed-dict-statement` is enabled by default
```

View File

@@ -2266,6 +2266,47 @@ def match_with_dict(u: Foo | Bar | dict):
reveal_type(u) # revealed: Foo | (dict[Unknown, Unknown] & ~<TypedDict with items 'tag'>)
```
## Only annotated declarations are allowed in the class body
<!-- snapshot-diagnostics -->
`TypedDict` class bodies are very restricted in what kinds of statements they can contain. Besides
annotated items, the only allowed statements are docstrings and `pass`. Annotated items are are also
not allowed to have a value.
```py
from typing import TypedDict
class Foo(TypedDict):
"""docstring"""
annotated_item: int
"""attribute docstring"""
pass
# As a non-standard but common extension, we interpret `...` as equivalent to `pass`.
...
class Bar(TypedDict):
a: int
# error: [invalid-typed-dict-statement] "invalid statement in TypedDict class body"
42
# error: [invalid-typed-dict-statement] "TypedDict item cannot have a value"
b: str = "hello"
# error: [invalid-typed-dict-statement] "TypedDict class cannot have methods"
def bar(self): ...
```
These rules are also enforced for `TypedDict` classes that don't directly inherit from `TypedDict`:
```py
class Baz(Bar):
# error: [invalid-typed-dict-statement]
def baz(self):
pass
```
[closed]: https://peps.python.org/pep-0728/#disallowing-extra-items-explicitly
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html

View File

@@ -4645,7 +4645,13 @@ impl<'db> Type<'db> {
let first_spec = specs_iter.next()?;
let mut builder = TupleSpecBuilder::from(&*first_spec);
for spec in specs_iter {
builder = builder.intersect(db, &spec);
// Two tuples cannot have incompatible specs unless the tuples themselves
// are disjoint. `IntersectionBuilder` eagerly simplifies such
// intersections to `Never`, so this should always return `Some`.
let Some(intersected) = builder.intersect(db, &spec) else {
return Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown())));
};
builder = intersected;
}
Some(Cow::Owned(builder.build()))
}

View File

@@ -1122,6 +1122,7 @@ impl<'db> Bindings<'db> {
class_literal.type_check_only(db),
Some(params),
class_literal.dataclass_transformer_params(db),
class_literal.total_ordering(db),
)));
}
}

View File

@@ -1516,6 +1516,9 @@ pub struct ClassLiteral<'db> {
pub(crate) dataclass_params: Option<DataclassParams<'db>>,
pub(crate) dataclass_transformer_params: Option<DataclassTransformerParams<'db>>,
/// Whether this class is decorated with `@functools.total_ordering`
pub(crate) total_ordering: bool,
}
// The Salsa heap is tracked separately.
@@ -1540,6 +1543,17 @@ impl<'db> ClassLiteral<'db> {
self.is_known(db, KnownClass::Tuple)
}
/// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`,
/// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if
/// synthesis is valid.
#[salsa::tracked]
pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool {
let body_scope = self.body_scope(db);
["__lt__", "__le__", "__gt__", "__ge__"]
.iter()
.any(|method| !class_member(db, body_scope, method).is_undefined())
}
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
// the knowledge that this class is not generic.
@@ -2384,6 +2398,41 @@ impl<'db> ClassLiteral<'db> {
) -> Option<Type<'db>> {
let dataclass_params = self.dataclass_params(db);
// Handle `@functools.total_ordering`: synthesize comparison methods
// for classes that have `@total_ordering` and define at least one
// ordering method. The decorator requires at least one of __lt__,
// __le__, __gt__, or __ge__ to be defined (either in this class or
// inherited from a superclass, excluding `object`).
if self.total_ordering(db) && matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__") {
// Check if any class in the MRO (excluding object) defines at least one
// ordering method in its own body (not synthesized).
let has_ordering_method = self
.iter_mro(db, specialization)
.filter_map(super::class_base::ClassBase::into_class)
.filter(|class| !class.class_literal(db).0.is_known(db, KnownClass::Object))
.any(|class| class.class_literal(db).0.has_own_ordering_method(db));
if has_ordering_method {
let instance_ty =
Type::instance(db, self.apply_optional_specialization(db, specialization));
let signature = Signature::new(
Parameters::new(
db,
[
Parameter::positional_or_keyword(Name::new_static("self"))
.with_annotated_type(instance_ty),
Parameter::positional_or_keyword(Name::new_static("other"))
.with_annotated_type(instance_ty),
],
),
Some(KnownClass::Bool.to_instance(db)),
);
return Some(Type::function_like_callable(db, signature));
}
}
let field_policy = CodeGeneratorKind::from_class(db, self, specialization)?;
let mut transformer_params =

View File

@@ -120,10 +120,13 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&REDUNDANT_CAST);
registry.register_lint(&UNRESOLVED_GLOBAL);
registry.register_lint(&MISSING_TYPED_DICT_KEY);
registry.register_lint(&INVALID_TYPED_DICT_STATEMENT);
registry.register_lint(&INVALID_METHOD_OVERRIDE);
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
registry.register_lint(&SUBCLASS_OF_DATACLASS_WITH_ORDER);
registry.register_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS);
registry.register_lint(&INVALID_TOTAL_ORDERING);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -1682,6 +1685,46 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for classes that inherit from a dataclass with `order=True`.
///
/// ## Why is this bad?
/// When a dataclass has `order=True`, comparison methods (`__lt__`, `__le__`, `__gt__`, `__ge__`)
/// are generated that compare instances as tuples of their fields. These methods raise a
/// `TypeError` at runtime when comparing instances of different classes in the inheritance
/// hierarchy, even if one is a subclass of the other.
///
/// This violates the [Liskov Substitution Principle] because child class instances cannot be
/// used in all contexts where parent class instances are expected.
///
/// ## Example
///
/// ```python
/// from dataclasses import dataclass
///
/// @dataclass(order=True)
/// class Parent:
/// value: int
///
/// class Child(Parent): # Ty emits a warning here
/// pass
///
/// # At runtime, this raises TypeError:
/// # Child(1) < Parent(2)
/// ```
///
/// Consider using [`functools.total_ordering`] instead, which does not have this limitation.
///
/// [Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle
/// [`functools.total_ordering`]: https://docs.python.org/3/library/functools.html#functools.total_ordering
pub(crate) static SUBCLASS_OF_DATACLASS_WITH_ORDER = {
summary: "detects subclasses of dataclasses with `order=True`",
status: LintStatus::stable("0.0.11"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for methods on subclasses that override superclass methods decorated with `@final`.
@@ -2167,6 +2210,31 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects statements other than annotated declarations in `TypedDict` class bodies.
///
/// ## Why is this bad?
/// `TypedDict` class bodies aren't allowed to contain any other types of statements. For
/// example, method definitions and field values aren't allowed. None of these will be
/// available on "instances of the `TypedDict`" at runtime (as `dict` is the runtime class of
/// all "`TypedDict` instances").
///
/// ## Example
/// ```python
/// from typing import TypedDict
///
/// class Foo(TypedDict):
/// def bar(self): # error: [invalid-typed-dict-statement]
/// pass
/// ```
pub(crate) static INVALID_TYPED_DICT_STATEMENT = {
summary: "detects invalid statements in `TypedDict` class bodies",
status: LintStatus::stable("0.0.9"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Detects method overrides that violate the [Liskov Substitution Principle] ("LSP").
@@ -2303,6 +2371,46 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for classes decorated with `@functools.total_ordering` that don't
/// define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).
///
/// ## Why is this bad?
/// The `@total_ordering` decorator requires the class to define at least one
/// ordering method. If none is defined, Python raises a `ValueError` at runtime.
///
/// ## Example
///
/// ```python
/// from functools import total_ordering
///
/// @total_ordering
/// class MyClass: # Error: no ordering method defined
/// def __eq__(self, other: object) -> bool:
/// return True
/// ```
///
/// Use instead:
///
/// ```python
/// from functools import total_ordering
///
/// @total_ordering
/// class MyClass:
/// def __eq__(self, other: object) -> bool:
/// return True
///
/// def __lt__(self, other: "MyClass") -> bool:
/// return True
/// ```
pub(crate) static INVALID_TOTAL_ORDERING = {
summary: "detects `@total_ordering` classes without an ordering method",
status: LintStatus::stable("0.0.10"),
default_level: Level::Error,
}
}
/// A collection of type check diagnostics.
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
pub struct TypeCheckDiagnostics {
@@ -4592,6 +4700,27 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
}
}
pub(super) fn report_invalid_total_ordering(
context: &InferContext<'_, '_>,
class: ClassLiteral<'_>,
decorator: &ast::Decorator,
) {
let db = context.db();
let Some(builder) = context.report_lint(&INVALID_TOTAL_ORDERING, decorator) else {
return;
};
let mut diagnostic = builder.into_diagnostic(
"Class decorated with `@total_ordering` must define at least one ordering method",
);
diagnostic.set_primary_message(format_args!(
"`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`",
class.name(db)
));
diagnostic.info("The decorator will raise `ValueError` at runtime");
}
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submodule.

View File

@@ -1413,6 +1413,9 @@ pub enum KnownFunction {
/// `dataclasses.field`
Field,
/// `functools.total_ordering`
TotalOrdering,
/// `inspect.getattr_static`
GetattrStatic,
@@ -1501,6 +1504,7 @@ impl KnownFunction {
Self::Dataclass | Self::Field => {
matches!(module, KnownModule::Dataclasses)
}
Self::TotalOrdering => module.is_functools(),
Self::GetattrStatic => module.is_inspect(),
Self::IsAssignableTo
| Self::IsDisjointFrom
@@ -2068,6 +2072,7 @@ pub(crate) mod tests {
KnownFunction::ImportModule => KnownModule::ImportLib,
KnownFunction::NamedTuple => KnownModule::Collections,
KnownFunction::TotalOrdering => KnownModule::Functools,
};
let function_definition = known_module_symbol(&db, module, function_name)

View File

@@ -63,11 +63,12 @@ use crate::types::diagnostic::{
INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NOT_SUBSCRIPTABLE,
POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_DATACLASS_WITH_ORDER, SUBCLASS_OF_FINAL_CLASS,
TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
@@ -77,7 +78,7 @@ use crate::types::diagnostic::{
report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_invalid_exception_tuple_caught,
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
report_invalid_or_unsupported_base, report_invalid_return_type,
report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering,
report_invalid_type_checking_constant, report_invalid_type_param_order,
report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable,
@@ -106,8 +107,8 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType,
CallableTypeKind, ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType,
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion,
CallableTypeKind, ClassLiteral, ClassType, DataclassFlags, DataclassParams, DynamicType,
InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion,
LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
@@ -779,6 +780,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
}
if let Some(base_params) = base_class_literal.dataclass_params(self.db()) {
if base_params.flags(self.db()).contains(DataclassFlags::ORDER) {
// Suppress the diagnostic if the child class overrides all comparison
// methods, since the user has explicitly fixed the LSP violation.
// This includes the case where the child class also has `order=True`,
// which generates all four comparison methods.
let dominated_methods = ["__lt__", "__le__", "__gt__", "__ge__"];
let child_has_order = class
.dataclass_params(self.db())
.is_some_and(|p| p.flags(self.db()).contains(DataclassFlags::ORDER));
let all_overridden = child_has_order
|| dominated_methods.iter().all(|method| {
!class
.own_class_member(self.db(), None, None, method)
.is_undefined()
});
if !all_overridden {
if let Some(builder) = self.context.report_lint(
&SUBCLASS_OF_DATACLASS_WITH_ORDER,
&class_node.bases()[i],
) {
let mut diagnostic = builder.into_diagnostic(format_args!(
"Class `{}` inherits from dataclass `{}` which has `order=True`",
class.name(self.db()),
base_class.name(self.db()),
));
diagnostic.info(
"Comparison of instances of the child class with instances \
of the parent class will raise `TypeError` at runtime",
);
}
}
}
}
}
// (4) Check that the class's MRO is resolvable
@@ -852,7 +889,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (5) Check that the class's metaclass can be determined without error.
// (5) Check that @total_ordering has a valid ordering method in the MRO
if class.total_ordering(self.db()) {
let has_ordering_method = class
.iter_mro(self.db(), None)
.filter_map(super::super::class_base::ClassBase::into_class)
.filter(|base_class| {
!base_class
.class_literal(self.db())
.0
.is_known(self.db(), KnownClass::Object)
})
.any(|base_class| {
base_class
.class_literal(self.db())
.0
.has_own_ordering_method(self.db())
});
if !has_ordering_method {
// Find the @total_ordering decorator to report the diagnostic at its location
if let Some(decorator) = class_node.decorator_list.iter().find(|decorator| {
self.expression_type(&decorator.expression)
.as_function_literal()
.is_some_and(|function| {
function.is_known(self.db(), KnownFunction::TotalOrdering)
})
}) {
report_invalid_total_ordering(&self.context, class, decorator);
}
}
}
// (6) Check that the class's metaclass can be determined without error.
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
match metaclass_error.reason() {
MetaclassErrorKind::Cycle => {
@@ -1054,6 +1123,67 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(protocol) = class.into_protocol_class(self.db()) {
protocol.validate_members(&self.context);
}
// (9) If it's a `TypedDict` class, check that it doesn't include any invalid
// statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax
//
// The body of the class definition defines the items of the `TypedDict` type. It
// may also contain a docstring or pass statements (primarily to allow the creation
// of an empty `TypedDict`). No other statements are allowed, and type checkers
// should report an error if any are present.
if class.is_typed_dict(self.db()) {
for stmt in &class_node.body {
match stmt {
// Annotated assignments are allowed (that's the whole point), but they're
// not allowed to have a value.
ast::Stmt::AnnAssign(ann_assign) => {
if let Some(value) = &ann_assign.value {
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPED_DICT_STATEMENT, &**value)
{
builder.into_diagnostic(format_args!(
"TypedDict item cannot have a value"
));
}
}
continue;
}
// Pass statements are allowed.
ast::Stmt::Pass(_) => continue,
ast::Stmt::Expr(expr) => {
// Docstrings are allowed.
if matches!(*expr.value, ast::Expr::StringLiteral(_)) {
continue;
}
// As a non-standard but common extension, we also interpret `...` as
// equivalent to `pass`.
if matches!(*expr.value, ast::Expr::EllipsisLiteral(_)) {
continue;
}
}
// Everything else is forbidden.
_ => {}
}
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPED_DICT_STATEMENT, stmt)
{
if matches!(stmt, ast::Stmt::FunctionDef(_)) {
builder.into_diagnostic(format_args!(
"TypedDict class cannot have methods"
));
} else {
let mut diagnostic = builder.into_diagnostic(format_args!(
"invalid statement in TypedDict class body"
));
diagnostic.info(
"Only annotated declarations (`<name>: <type>`) are allowed.",
);
}
}
}
}
}
}
@@ -2803,6 +2933,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut type_check_only = false;
let mut dataclass_params = None;
let mut dataclass_transformer_params = None;
let mut total_ordering = false;
for decorator in decorator_list {
let decorator_ty = self.infer_decorator(decorator);
if decorator_ty
@@ -2813,6 +2944,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
continue;
}
if decorator_ty
.as_function_literal()
.is_some_and(|function| function.is_known(self.db(), KnownFunction::TotalOrdering))
{
total_ordering = true;
continue;
}
if let Type::DataclassDecorator(params) = decorator_ty {
dataclass_params = Some(params);
continue;
@@ -2900,6 +3039,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
type_check_only,
dataclass_params,
dataclass_transformer_params,
total_ordering,
)),
};

View File

@@ -357,124 +357,6 @@ impl<'db> Type<'db> {
}
match (self, target) {
// These branches here are quick optimisations to exclude
// fully static variants that we know are always mutually disjoint.
(
Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
| Type::EnumLiteral(..)
| Type::WrapperDescriptor(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
| Type::SpecialForm(..),
Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
| Type::EnumLiteral(..)
| Type::WrapperDescriptor(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
| Type::SpecialForm(..),
) => ConstraintSet::from(self == target),
(
Type::TypeIs(..)
| Type::TypeGuard(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
Type::LiteralString,
)
| (
Type::LiteralString,
Type::TypeIs(..)
| Type::TypeGuard(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
)
| (
Type::TypedDict(..),
Type::LiteralString
| Type::TypeIs(..)
| Type::TypeGuard(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
)
| (
Type::TypeGuard(..) | Type::LiteralString | Type::TypeIs(..),
Type::TypedDict(..) | Type::BoundSuper(_) | Type::PropertyInstance(_),
)
| (
Type::TypeIs(..)
| Type::TypeGuard(..)
| Type::LiteralString
| Type::TypedDict(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
Type::ClassLiteral(..)
| Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::BytesLiteral(..)
| Type::EnumLiteral(..)
| Type::WrapperDescriptor(..)
| Type::ModuleLiteral(..)
| Type::SpecialForm(..)
| Type::SubclassOf(..),
)
| (
Type::ClassLiteral(..)
| Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::BytesLiteral(..)
| Type::EnumLiteral(..)
| Type::WrapperDescriptor(..)
| Type::ModuleLiteral(..)
| Type::SubclassOf(_)
| Type::SpecialForm(..),
Type::TypeIs(..)
| Type::TypeGuard(..)
| Type::LiteralString
| Type::TypedDict(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
)
| (
Type::SubclassOf(..) | Type::GenericAlias(..),
Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::LiteralString
| Type::BytesLiteral(..)
| Type::EnumLiteral(..)
| Type::FunctionLiteral(..)
| Type::BoundMethod(..)
| Type::KnownBoundMethod(..)
| Type::WrapperDescriptor(..)
| Type::TypedDict(..)
| Type::ModuleLiteral(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
)
| (
Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::LiteralString
| Type::BytesLiteral(..)
| Type::EnumLiteral(..)
| Type::FunctionLiteral(..)
| Type::BoundMethod(..)
| Type::KnownBoundMethod(..)
| Type::TypedDict(..)
| Type::WrapperDescriptor(..)
| Type::ModuleLiteral(..)
| Type::BoundSuper(_)
| Type::PropertyInstance(_),
Type::SubclassOf(..) | Type::GenericAlias(..),
) => ConstraintSet::from(false),
// Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.is_object() => {
ConstraintSet::from(true)
@@ -958,6 +840,27 @@ impl<'db> Type<'db> {
)
}
// No literal type is a subtype of any other literal type, unless they are the same
// type (which is handled above). This case is not necessary from a correctness
// perspective (the fallback cases below will handle it correctly), but it is important
// for performance of simplifying large unions of literal types.
(
Type::StringLiteral(_)
| Type::IntLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::EnumLiteral(_),
Type::StringLiteral(_)
| Type::IntLiteral(_)
| Type::BytesLiteral(_)
| Type::ClassLiteral(_)
| Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::EnumLiteral(_),
) => ConstraintSet::from(false),
(Type::Callable(self_callable), Type::Callable(other_callable)) => relation_visitor
.visit((self, target, relation), || {
self_callable.has_relation_to_impl(

View File

@@ -1931,12 +1931,14 @@ impl<'db> TupleSpecBuilder<'db> {
}
}
/// Return a new tuple-spec builder that reflects the intersection of this tuple and another tuple.
/// Return a new tuple-spec builder that reflects the intersection of this tuple and another
/// tuple, or `None` if the intersection is impossible (e.g., two fixed-length tuples with
/// different lengths).
///
/// For example, if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a
/// tuple-spec for `tuple[object, object]`, the result will be a tuple-spec builder for
/// `tuple[int, str]` (since `int & object` simplifies to `int`, and `str & object` to `str`).
pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self {
pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Option<Self> {
match (&mut self, other) {
// Both fixed-length with the same length: element-wise intersection.
(TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements))
@@ -1945,24 +1947,23 @@ impl<'db> TupleSpecBuilder<'db> {
for (existing, new) in our_elements.iter_mut().zip(new_elements.all_elements()) {
*existing = IntersectionType::from_elements(db, [*existing, *new]);
}
return self;
Some(self)
}
(TupleSpecBuilder::Fixed(our_elements), TupleSpec::Variable(var)) => {
if let Ok(tuple) = var.resize(db, TupleLength::Fixed(our_elements.len())) {
return self.intersect(db, &tuple);
}
}
// Fixed-length tuples with different lengths cannot intersect.
(TupleSpecBuilder::Fixed(_), TupleSpec::Fixed(_)) => None,
(TupleSpecBuilder::Variable { .. }, TupleSpec::Fixed(fixed)) => {
if let Ok(tuple) = self
.clone()
.build()
.resize(db, TupleLength::Fixed(fixed.len()))
{
return TupleSpecBuilder::from(&tuple).intersect(db, other);
}
}
(TupleSpecBuilder::Fixed(our_elements), TupleSpec::Variable(var)) => var
.resize(db, TupleLength::Fixed(our_elements.len()))
.ok()
.and_then(|tuple| self.intersect(db, &tuple)),
(TupleSpecBuilder::Variable { .. }, TupleSpec::Fixed(fixed)) => self
.clone()
.build()
.resize(db, TupleLength::Fixed(fixed.len()))
.ok()
.and_then(|tuple| TupleSpecBuilder::from(&tuple).intersect(db, other)),
(
TupleSpecBuilder::Variable {
@@ -1982,29 +1983,20 @@ impl<'db> TupleSpecBuilder<'db> {
for (existing, new) in suffix.iter_mut().zip(var.suffix_elements()) {
*existing = IntersectionType::from_elements(db, [*existing, *new]);
}
return self;
return Some(self);
}
let self_built = self.clone().build();
let self_len = self_built.len();
if let Ok(resized) = var.resize(db, self_len) {
return self.intersect(db, &resized);
} else if let Ok(resized) = self_built.resize(db, var.len()) {
return TupleSpecBuilder::from(&resized).intersect(db, other);
}
var.resize(db, self_len)
.ok()
.and_then(|resized| self.intersect(db, &resized))
.or_else(|| {
self_built.resize(db, var.len()).ok().and_then(|resized| {
TupleSpecBuilder::from(&resized).intersect(db, other)
})
})
}
_ => {}
}
// TODO: probably incorrect? `tuple[int, str] & tuple[int, str, bytes]` should resolve to `Never`.
// So maybe this function should be fallible (return an `Option`)?
let intersected =
IntersectionType::from_elements(db, self.all_elements().chain(other.all_elements()));
TupleSpecBuilder::Variable {
prefix: vec![],
variable: intersected,
suffix: vec![],
}
}

View File

@@ -1575,6 +1575,9 @@ impl DocumentHandle {
{
db.project().remove_file(db, file);
}
// Bump the file's revision back to using the file system's revision.
file.sync(db);
} else {
// This can only fail when the path is a directory or it doesn't exists but the
// file should exists for this handler in this branch. This is because every
@@ -1598,6 +1601,8 @@ impl DocumentHandle {
if let Some(virtual_file) = db.files().try_virtual_file(virtual_path) {
db.project().close_file(db, virtual_file.file());
virtual_file.close(db);
// Bump the file's revision back to using the file system's revision.
virtual_file.sync(db);
} else {
tracing::warn!("Salsa virtual file does not exists for {}", virtual_path);
}

View File

@@ -70,3 +70,40 @@ fn multiline_token_client_supporting_multiline_tokens() -> Result<()> {
Ok(())
}
// Regression test for https://github.com/astral-sh/ty/issues/2346
#[test]
fn no_stale_tokens_after_opening_the_same_file_with_new_content() -> Result<()> {
let file_name = "src/foo";
let initial_content =
"def calculate_sum(a):\n # Version A: Basic math\n return a\n\nresult = calculate_sum(5)\n";
let mut server = TestServerBuilder::new()?
.enable_pull_diagnostics(true)
.enable_multiline_token_support(true)
.with_workspace(SystemPath::new("src"), None)?
.with_file(file_name, initial_content)?
.build()
.wait_until_workspaces_are_initialized();
server.open_text_document(file_name, initial_content, 0);
let initial_tokens = server
.semantic_tokens_full_request(&server.file_uri(file_name))
.unwrap();
server.close_text_document(file_name);
server.open_text_document(
file_name,
"# Version B: Basic greeting\ndef say_hello():\n print(\"Hello, World!\")\n\nsay_hello()\n",
0,
);
let new_tokens = server
.semantic_tokens_full_request(&server.file_uri(file_name))
.unwrap();
assert_ne!(initial_tokens, new_tokens);
Ok(())
}

View File

@@ -428,6 +428,7 @@ impl Workspace {
Ok(completions
.into_iter()
.map(|comp| {
let name = comp.insert.as_deref().unwrap_or(&comp.name).to_string();
let kind = comp.kind(&self.db).map(CompletionKind::from);
let type_display = comp.ty.map(|ty| ty.display(&self.db).to_string());
let import_edit = comp.import.as_ref().map(|edit| {
@@ -443,7 +444,7 @@ impl Workspace {
}
});
Completion {
name: comp.name.into(),
name,
kind,
detail: type_display,
module_name: comp.module_name.map(ToString::to_string),

View File

@@ -6,6 +6,7 @@ import {
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react";
import { ErrorMessage, Header, setupMonaco, useTheme } from "shared";
@@ -24,15 +25,22 @@ export default function Playground() {
const [workspace, setWorkspace] = useState<Workspace | null>(null);
const [files, dispatchFiles] = useReducer(filesReducer, INIT_FILES_STATE);
const [workspacePromise] = useState<Promise<Workspace>>(() =>
startPlayground().then((fetched) => {
const workspacePromiseRef = useRef<Promise<Workspace> | null>(null);
if (workspacePromiseRef.current == null) {
workspacePromiseRef.current = startPlayground().then((fetched) => {
setVersion(fetched.version);
const workspace = new Workspace("/", PositionEncoding.Utf16, {});
restoreWorkspace(workspace, fetched.workspace, dispatchFiles, setError);
setWorkspace(workspace);
return workspace;
}),
);
});
}
// This is safe as this is only called once on startup.
// We need useRef to avoid duplicate initialization when
// running locally due to react rendering
// everything twice in strict mode in debug builds.
// eslint-disable-next-line react-hooks/refs
const workspacePromise = workspacePromiseRef.current;
const fileName = useMemo(() => {
return (

30
ty.schema.json generated
View File

@@ -786,6 +786,16 @@
}
]
},
"invalid-total-ordering": {
"title": "detects `@total_ordering` classes without an ordering method",
"description": "## What it does\nChecks for classes decorated with `@functools.total_ordering` that don't\ndefine any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).\n\n## Why is this bad?\nThe `@total_ordering` decorator requires the class to define at least one\nordering method. If none is defined, Python raises a `ValueError` at runtime.\n\n## Example\n\n```python\nfrom functools import total_ordering\n\n@total_ordering\nclass MyClass: # Error: no ordering method defined\n def __eq__(self, other: object) -> bool:\n return True\n```\n\nUse instead:\n\n```python\nfrom functools import total_ordering\n\n@total_ordering\nclass MyClass:\n def __eq__(self, other: object) -> bool:\n return True\n\n def __lt__(self, other: \"MyClass\") -> bool:\n return True\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-type-alias-type": {
"title": "detects invalid TypeAliasType definitions",
"description": "## What it does\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n```python\nfrom typing import TypeAliasType\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\nNewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name must be a string literal\n```",
@@ -856,6 +866,16 @@
}
]
},
"invalid-typed-dict-statement": {
"title": "detects invalid statements in `TypedDict` class bodies",
"description": "## What it does\nDetects statements other than annotated declarations in `TypedDict` class bodies.\n\n## Why is this bad?\n`TypedDict` class bodies aren't allowed to contain any other types of statements. For\nexample, method definitions and field values aren't allowed. None of these will be\navailable on \"instances of the `TypedDict`\" at runtime (as `dict` is the runtime class of\nall \"`TypedDict` instances\").\n\n## Example\n```python\nfrom typing import TypedDict\n\nclass Foo(TypedDict):\n def bar(self): # error: [invalid-typed-dict-statement]\n pass\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"missing-argument": {
"title": "detects missing required arguments in a call",
"description": "## What it does\nChecks for missing required arguments in a call.\n\n## Why is this bad?\nFailing to provide a required argument will raise a `TypeError` at runtime.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc() # TypeError: func() missing 1 required positional argument: 'x'\n```",
@@ -1006,6 +1026,16 @@
}
]
},
"subclass-of-dataclass-with-order": {
"title": "detects subclasses of dataclasses with `order=True`",
"description": "## What it does\nChecks for classes that inherit from a dataclass with `order=True`.\n\n## Why is this bad?\nWhen a dataclass has `order=True`, comparison methods (`__lt__`, `__le__`, `__gt__`, `__ge__`)\nare generated that compare instances as tuples of their fields. These methods raise a\n`TypeError` at runtime when comparing instances of different classes in the inheritance\nhierarchy, even if one is a subclass of the other.\n\nThis violates the [Liskov Substitution Principle] because child class instances cannot be\nused in all contexts where parent class instances are expected.\n\n## Example\n\n```python\nfrom dataclasses import dataclass\n\n@dataclass(order=True)\nclass Parent:\n value: int\n\nclass Child(Parent): # Ty emits a warning here\n pass\n\n# At runtime, this raises TypeError:\n# Child(1) < Parent(2)\n```\n\nConsider using [`functools.total_ordering`] instead, which does not have this limitation.\n\n[Liskov Substitution Principle]: https://en.wikipedia.org/wiki/Liskov_substitution_principle\n[`functools.total_ordering`]: https://docs.python.org/3/library/functools.html#functools.total_ordering",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"subclass-of-final-class": {
"title": "detects subclasses of final classes",
"description": "## What it does\nChecks for classes that subclass final classes.\n\n## Why is this bad?\nDecorating a class with `@final` declares to the type checker that it should not be subclassed.\n\n## Example\n\n```python\nfrom typing import final\n\n@final\nclass A: ...\nclass B(A): ... # Error raised here\n```",