Compare commits
14 Commits
alex/relat
...
charlie/or
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634765c303 | ||
|
|
d65542c05e | ||
|
|
98728b2c98 | ||
|
|
924b2972f2 | ||
|
|
d035744959 | ||
|
|
ce059c4857 | ||
|
|
acbc83d6d2 | ||
|
|
a9e5246786 | ||
|
|
8b8b174e4f | ||
|
|
28fa02129b | ||
|
|
a10e42294b | ||
|
|
12a4ca003f | ||
|
|
60f7ec90ef | ||
|
|
922d964bcb |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", {})
|
||||
)
|
||||
|
||||
@@ -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", {})
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.options.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py
vendored
Normal file
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py
vendored
Normal 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
|
||||
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py.expect
vendored
Normal file
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py.expect
vendored
Normal 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
|
||||
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.options.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
28
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py
vendored
Normal file
28
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py
vendored
Normal 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",},
|
||||
)
|
||||
32
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py.expect
vendored
Normal file
32
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py.expect
vendored
Normal 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",
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_in_clause.py
vendored
Normal file
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_in_clause.py
vendored
Normal 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
|
||||
@@ -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
|
||||
35
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_strings.py
vendored
Normal file
35
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_strings.py
vendored
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py
vendored
Normal file
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py
vendored
Normal 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
|
||||
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py.expect
vendored
Normal file
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py.expect
vendored
Normal 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
|
||||
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.options.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"target_version": "3.14"}
|
||||
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py
vendored
Normal file
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py
vendored
Normal 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 ()'''}
|
||||
'''
|
||||
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py.expect
vendored
Normal file
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py.expect
vendored
Normal 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 ()'''}
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_fmtpass_imports.py
vendored
Normal file
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_fmtpass_imports.py
vendored
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
16
crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_parens_from_lhs.py
vendored
Normal file
16
crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_parens_from_lhs.py
vendored
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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", {})
|
||||
)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
@@ -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 ()'''}
|
||||
"""
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
275
crates/ty/docs/rules.md
generated
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = false
|
||||
@@ -0,0 +1 @@
|
||||
class Foo(m<CURSOR: metaclass>)
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/class-arg-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/class-arg-completion/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -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=
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
Some details
|
||||
`````python
|
||||
x_y = thing_do();
|
||||
``` # this should't close the fence!
|
||||
a_b = other_thing();
|
||||
`````
|
||||
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:
|
||||
Some details
|
||||
~~~~~~python
|
||||
x_y = thing_do();
|
||||
~~~ # this should't close the fence!
|
||||
a_b = other_thing();
|
||||
~~~~~~
|
||||
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
|
||||
");
|
||||
|
||||
@@ -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)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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): ...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)),
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
30
ty.schema.json
generated
@@ -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```",
|
||||
|
||||
Reference in New Issue
Block a user