Compare commits
38 Commits
cjm/callab
...
gankra/dun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6caa37fb27 | ||
|
|
cf8d2e35a8 | ||
|
|
0290f5dc3b | ||
|
|
5bb9ee2a9d | ||
|
|
638f230910 | ||
|
|
b36ff75a24 | ||
|
|
30c3f9aafe | ||
|
|
883701ae88 | ||
|
|
0bd7a94c27 | ||
|
|
421f88bb32 | ||
|
|
b0eb39d112 | ||
|
|
260f463edd | ||
|
|
52849a5e68 | ||
|
|
2a61fe2353 | ||
|
|
764ad8b29b | ||
|
|
85af715880 | ||
|
|
b0bc990cbf | ||
|
|
ad3de4e488 | ||
|
|
2214a46139 | ||
|
|
c02bd11b93 | ||
|
|
eeaaa8e9fe | ||
|
|
7f7485d608 | ||
|
|
d755f3b522 | ||
|
|
83168a1bb1 | ||
|
|
0f373603eb | ||
|
|
cc23af944f | ||
|
|
0589700ca1 | ||
|
|
43d983ecae | ||
|
|
5c69bb564c | ||
|
|
89fed85a8d | ||
|
|
051f6896ac | ||
|
|
5b1d3ac9b9 | ||
|
|
b2b0ad38ea | ||
|
|
01c0a3e960 | ||
|
|
5c942119f8 | ||
|
|
2acf1cc0fd | ||
|
|
4fdbe26445 | ||
|
|
682d29c256 |
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
@@ -57,8 +57,11 @@ Ruff is extremely actively developed and used in major open-source projects like
|
||||
|
||||
...and [many more](#whos-using-ruff).
|
||||
|
||||
Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff),
|
||||
or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
|
||||
Ruff is backed by [Astral](https://astral.sh), the creators of
|
||||
[uv](https://github.com/astral-sh/uv) and [ty](https://github.com/astral-sh/ty).
|
||||
|
||||
Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff), or the
|
||||
original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
|
||||
|
||||
## Testimonials
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ extend-exclude = [
|
||||
"crates/ty_vendored/vendor/**/*",
|
||||
"**/resources/**/*",
|
||||
"**/snapshots/**/*",
|
||||
"crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs",
|
||||
# Completion tests tend to have a lot of incomplete
|
||||
# words naturally. It's annoying to have to make all
|
||||
# of them actually words. So just ignore typos here.
|
||||
|
||||
@@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
13030,
|
||||
13100,
|
||||
);
|
||||
|
||||
static TANJUN: Benchmark = Benchmark::new(
|
||||
@@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
950,
|
||||
1100,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
66
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py
vendored
Normal file
66
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
facts = (
|
||||
"Lobsters have blue blood.",
|
||||
"The liver is the only human organ that can fully regenerate itself.",
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
)
|
||||
|
||||
facts = [
|
||||
"Lobsters have blue blood.",
|
||||
"The liver is the only human organ that can fully regenerate itself.",
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
]
|
||||
|
||||
facts = {
|
||||
"Lobsters have blue blood.",
|
||||
"The liver is the only human organ that can fully regenerate itself.",
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
}
|
||||
|
||||
facts = {
|
||||
(
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon."
|
||||
),
|
||||
}
|
||||
|
||||
facts = (
|
||||
"Octopuses have three hearts."
|
||||
# Missing comma here.
|
||||
"Honey never spoils.",
|
||||
)
|
||||
|
||||
facts = [
|
||||
"Octopuses have three hearts."
|
||||
# Missing comma here.
|
||||
"Honey never spoils.",
|
||||
]
|
||||
|
||||
facts = {
|
||||
"Octopuses have three hearts."
|
||||
# Missing comma here.
|
||||
"Honey never spoils.",
|
||||
}
|
||||
|
||||
facts = (
|
||||
(
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon."
|
||||
),
|
||||
)
|
||||
|
||||
facts = [
|
||||
(
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon."
|
||||
),
|
||||
]
|
||||
|
||||
facts = (
|
||||
"Lobsters have blue blood.\n"
|
||||
"The liver is the only human organ that can fully regenerate itself.\n"
|
||||
"Clarinets are made almost entirely out of wood from the mpingo tree.\n"
|
||||
"In 1971, astronaut Alan Shepard played golf on the moon.\n"
|
||||
)
|
||||
@@ -136,4 +136,38 @@ os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
||||
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/21794
|
||||
import sys
|
||||
|
||||
if os.rename("pth1.py", "pth1.py.bak"):
|
||||
print("rename: truthy")
|
||||
else:
|
||||
print("rename: falsey")
|
||||
|
||||
if os.replace("pth1.py.bak", "pth1.py"):
|
||||
print("replace: truthy")
|
||||
else:
|
||||
print("replace: falsey")
|
||||
|
||||
try:
|
||||
for _ in os.getcwd():
|
||||
print("getcwd: iterable")
|
||||
break
|
||||
except TypeError as e:
|
||||
print("getcwd: not iterable")
|
||||
|
||||
try:
|
||||
for _ in os.getcwdb():
|
||||
print("getcwdb: iterable")
|
||||
break
|
||||
except TypeError as e:
|
||||
print("getcwdb: not iterable")
|
||||
|
||||
try:
|
||||
for _ in os.readlink(sys.executable):
|
||||
print("readlink: iterable")
|
||||
break
|
||||
except TypeError as e:
|
||||
print("readlink: not iterable")
|
||||
|
||||
@@ -132,7 +132,6 @@ async def c():
|
||||
# Non-errors
|
||||
###
|
||||
|
||||
# False-negative: RustPython doesn't parse the `\N{snowman}`.
|
||||
"\N{snowman} {}".format(a)
|
||||
|
||||
"{".format(a)
|
||||
@@ -276,3 +275,6 @@ if __name__ == "__main__":
|
||||
number = 0
|
||||
string = "{}".format(number := number + 1)
|
||||
print(string)
|
||||
|
||||
# Unicode escape
|
||||
"\N{angle}AOB = {angle}°".format(angle=180)
|
||||
|
||||
@@ -138,5 +138,6 @@ with open("file.txt", encoding="utf-8") as f:
|
||||
with open("file.txt", encoding="utf-8") as f:
|
||||
contents = process_contents(f.read())
|
||||
|
||||
with open("file.txt", encoding="utf-8") as f:
|
||||
with open("file1.txt", encoding="utf-8") as f:
|
||||
contents: str = process_contents(f.read())
|
||||
|
||||
8
crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
with Path("file.txt").open() as f:
|
||||
contents = f.read()
|
||||
|
||||
with Path("file.txt").open("r") as f:
|
||||
contents = f.read()
|
||||
26
crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py
vendored
Normal file
26
crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
|
||||
with Path("file.txt").open("w") as f:
|
||||
f.write("test")
|
||||
|
||||
with Path("file.txt").open("wb") as f:
|
||||
f.write(b"test")
|
||||
|
||||
with Path("file.txt").open(mode="w") as f:
|
||||
f.write("test")
|
||||
|
||||
with Path("file.txt").open("w", encoding="utf8") as f:
|
||||
f.write("test")
|
||||
|
||||
with Path("file.txt").open("w", errors="ignore") as f:
|
||||
f.write("test")
|
||||
|
||||
with Path(foo()).open("w") as f:
|
||||
f.write("test")
|
||||
|
||||
p = Path("file.txt")
|
||||
with p.open("w") as f:
|
||||
f.write("test")
|
||||
|
||||
with Path("foo", "bar", "baz").open("w") as f:
|
||||
f.write("test")
|
||||
38
crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py
vendored
Normal file
38
crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
a: int = 1
|
||||
def f1():
|
||||
global a
|
||||
a: str = "foo" # error
|
||||
|
||||
b: int = 1
|
||||
def outer():
|
||||
def inner():
|
||||
global b
|
||||
b: str = "nested" # error
|
||||
|
||||
c: int = 1
|
||||
def f2():
|
||||
global c
|
||||
c: list[str] = [] # error
|
||||
|
||||
d: int = 1
|
||||
def f3():
|
||||
global d
|
||||
d: str # error
|
||||
|
||||
e: int = 1
|
||||
def f4():
|
||||
e: str = "happy" # okay
|
||||
|
||||
global f
|
||||
f: int = 1 # okay
|
||||
|
||||
g: int = 1
|
||||
global g # error
|
||||
|
||||
class C:
|
||||
x: str
|
||||
global x # error
|
||||
|
||||
class D:
|
||||
global x # error
|
||||
x: str
|
||||
@@ -214,6 +214,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
range: _,
|
||||
node_index: _,
|
||||
}) => {
|
||||
if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) {
|
||||
flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal(
|
||||
checker,
|
||||
expr,
|
||||
elts,
|
||||
);
|
||||
}
|
||||
if ctx.is_store() {
|
||||
let check_too_many_expressions =
|
||||
checker.is_rule_enabled(Rule::ExpressionsInStarAssignment);
|
||||
@@ -1329,6 +1336,13 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
}
|
||||
}
|
||||
Expr::Set(set) => {
|
||||
if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) {
|
||||
flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal(
|
||||
checker,
|
||||
expr,
|
||||
&set.elts,
|
||||
);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::DuplicateValue) {
|
||||
flake8_bugbear::rules::duplicate_value(checker, set);
|
||||
}
|
||||
|
||||
@@ -454,6 +454,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8ImplicitStrConcat, "001") => rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation,
|
||||
(Flake8ImplicitStrConcat, "002") => rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation,
|
||||
(Flake8ImplicitStrConcat, "003") => rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation,
|
||||
(Flake8ImplicitStrConcat, "004") => rules::flake8_implicit_str_concat::rules::ImplicitStringConcatenationInCollectionLiteral,
|
||||
|
||||
// flake8-print
|
||||
(Flake8Print, "1") => rules::flake8_print::rules::Print,
|
||||
|
||||
@@ -1001,6 +1001,7 @@ mod tests {
|
||||
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)]
|
||||
#[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)]
|
||||
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"semantic_syntax_error_{}_{}",
|
||||
|
||||
@@ -70,7 +70,7 @@ fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
}
|
||||
|
||||
/// Returns `true` if an expression resolves to a call to `pathlib.Path.open`.
|
||||
fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
pub(crate) fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ mod async_zero_sleep;
|
||||
mod blocking_http_call;
|
||||
mod blocking_http_call_httpx;
|
||||
mod blocking_input;
|
||||
mod blocking_open_call;
|
||||
pub(crate) mod blocking_open_call;
|
||||
mod blocking_path_methods;
|
||||
mod blocking_process_invocation;
|
||||
mod blocking_sleep;
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{checkers::ast::Checker, settings::LinterSettings};
|
||||
/// Checks for non-literal strings being passed to [`markupsafe.Markup`][markupsafe-markup].
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// [`markupsafe.Markup`] does not perform any escaping, so passing dynamic
|
||||
/// [`markupsafe.Markup`][markupsafe-markup] does not perform any escaping, so passing dynamic
|
||||
/// content, like f-strings, variables or interpolated strings will potentially
|
||||
/// lead to XSS vulnerabilities.
|
||||
///
|
||||
|
||||
@@ -32,6 +32,10 @@ mod tests {
|
||||
Path::new("ISC_syntax_error_2.py")
|
||||
)]
|
||||
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))]
|
||||
#[test_case(
|
||||
Rule::ImplicitStringConcatenationInCollectionLiteral,
|
||||
Path::new("ISC004.py")
|
||||
)]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::token::parenthesized_range;
|
||||
use ruff_python_ast::{Expr, StringLike};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for implicitly concatenated strings inside list, tuple, and set literals.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In collection literals, implicit string concatenation is often the result of
|
||||
/// a missing comma between elements, which can silently merge items together.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// facts = (
|
||||
/// "Lobsters have blue blood.",
|
||||
/// "The liver is the only human organ that can fully regenerate itself.",
|
||||
/// "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
/// "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Instead, you likely intended:
|
||||
/// ```python
|
||||
/// facts = (
|
||||
/// "Lobsters have blue blood.",
|
||||
/// "The liver is the only human organ that can fully regenerate itself.",
|
||||
/// "Clarinets are made almost entirely out of wood from the mpingo tree.",
|
||||
/// "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// If the concatenation is intentional, wrap it in parentheses to make it
|
||||
/// explicit:
|
||||
/// ```python
|
||||
/// facts = (
|
||||
/// "Lobsters have blue blood.",
|
||||
/// "The liver is the only human organ that can fully regenerate itself.",
|
||||
/// (
|
||||
/// "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
/// "In 1971, astronaut Alan Shepard played golf on the moon."
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix is safe in that it does not change the semantics of your code.
|
||||
/// However, the issue is that you may often want to change semantics
|
||||
/// by adding a missing comma.
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.10")]
|
||||
pub(crate) struct ImplicitStringConcatenationInCollectionLiteral;
|
||||
|
||||
impl Violation for ImplicitStringConcatenationInCollectionLiteral {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Unparenthesized implicit string concatenation in collection".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Wrap implicitly concatenated strings in parentheses".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// ISC004
|
||||
pub(crate) fn implicit_string_concatenation_in_collection_literal(
|
||||
checker: &Checker,
|
||||
expr: &Expr,
|
||||
elements: &[Expr],
|
||||
) {
|
||||
for element in elements {
|
||||
let Ok(string_like) = StringLike::try_from(element) else {
|
||||
continue;
|
||||
};
|
||||
if !string_like.is_implicit_concatenated() {
|
||||
continue;
|
||||
}
|
||||
if parenthesized_range(
|
||||
string_like.as_expression_ref(),
|
||||
expr.into(),
|
||||
checker.tokens(),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
ImplicitStringConcatenationInCollectionLiteral,
|
||||
string_like.range(),
|
||||
);
|
||||
diagnostic.help("Did you forget a comma?");
|
||||
diagnostic.set_fix(Fix::unsafe_edits(
|
||||
Edit::insertion("(".to_string(), string_like.range().start()),
|
||||
[Edit::insertion(")".to_string(), string_like.range().end())],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
pub(crate) use collection_literal::*;
|
||||
pub(crate) use explicit::*;
|
||||
pub(crate) use implicit::*;
|
||||
|
||||
mod collection_literal;
|
||||
mod explicit;
|
||||
mod implicit;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
|
||||
---
|
||||
ISC004 [*] Unparenthesized implicit string concatenation in collection
|
||||
--> ISC004.py:4:5
|
||||
|
|
||||
2 | "Lobsters have blue blood.",
|
||||
3 | "The liver is the only human organ that can fully regenerate itself.",
|
||||
4 | / "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
5 | | "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
| |______________________________________________________________^
|
||||
6 | )
|
||||
|
|
||||
help: Wrap implicitly concatenated strings in parentheses
|
||||
help: Did you forget a comma?
|
||||
1 | facts = (
|
||||
2 | "Lobsters have blue blood.",
|
||||
3 | "The liver is the only human organ that can fully regenerate itself.",
|
||||
- "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
- "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
4 + ("Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
5 + "In 1971, astronaut Alan Shepard played golf on the moon."),
|
||||
6 | )
|
||||
7 |
|
||||
8 | facts = [
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ISC004 [*] Unparenthesized implicit string concatenation in collection
|
||||
--> ISC004.py:11:5
|
||||
|
|
||||
9 | "Lobsters have blue blood.",
|
||||
10 | "The liver is the only human organ that can fully regenerate itself.",
|
||||
11 | / "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
12 | | "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
| |______________________________________________________________^
|
||||
13 | ]
|
||||
|
|
||||
help: Wrap implicitly concatenated strings in parentheses
|
||||
help: Did you forget a comma?
|
||||
8 | facts = [
|
||||
9 | "Lobsters have blue blood.",
|
||||
10 | "The liver is the only human organ that can fully regenerate itself.",
|
||||
- "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
- "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
11 + ("Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
12 + "In 1971, astronaut Alan Shepard played golf on the moon."),
|
||||
13 | ]
|
||||
14 |
|
||||
15 | facts = {
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ISC004 [*] Unparenthesized implicit string concatenation in collection
|
||||
--> ISC004.py:18:5
|
||||
|
|
||||
16 | "Lobsters have blue blood.",
|
||||
17 | "The liver is the only human organ that can fully regenerate itself.",
|
||||
18 | / "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
19 | | "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
| |______________________________________________________________^
|
||||
20 | }
|
||||
|
|
||||
help: Wrap implicitly concatenated strings in parentheses
|
||||
help: Did you forget a comma?
|
||||
15 | facts = {
|
||||
16 | "Lobsters have blue blood.",
|
||||
17 | "The liver is the only human organ that can fully regenerate itself.",
|
||||
- "Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
- "In 1971, astronaut Alan Shepard played golf on the moon.",
|
||||
18 + ("Clarinets are made almost entirely out of wood from the mpingo tree."
|
||||
19 + "In 1971, astronaut Alan Shepard played golf on the moon."),
|
||||
20 | }
|
||||
21 |
|
||||
22 | facts = {
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ISC004 [*] Unparenthesized implicit string concatenation in collection
|
||||
--> ISC004.py:30:5
|
||||
|
|
||||
29 | facts = (
|
||||
30 | / "Octopuses have three hearts."
|
||||
31 | | # Missing comma here.
|
||||
32 | | "Honey never spoils.",
|
||||
| |_________________________^
|
||||
33 | )
|
||||
|
|
||||
help: Wrap implicitly concatenated strings in parentheses
|
||||
help: Did you forget a comma?
|
||||
27 | }
|
||||
28 |
|
||||
29 | facts = (
|
||||
- "Octopuses have three hearts."
|
||||
30 + ("Octopuses have three hearts."
|
||||
31 | # Missing comma here.
|
||||
- "Honey never spoils.",
|
||||
32 + "Honey never spoils."),
|
||||
33 | )
|
||||
34 |
|
||||
35 | facts = [
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ISC004 [*] Unparenthesized implicit string concatenation in collection
|
||||
--> ISC004.py:36:5
|
||||
|
|
||||
35 | facts = [
|
||||
36 | / "Octopuses have three hearts."
|
||||
37 | | # Missing comma here.
|
||||
38 | | "Honey never spoils.",
|
||||
| |_________________________^
|
||||
39 | ]
|
||||
|
|
||||
help: Wrap implicitly concatenated strings in parentheses
|
||||
help: Did you forget a comma?
|
||||
33 | )
|
||||
34 |
|
||||
35 | facts = [
|
||||
- "Octopuses have three hearts."
|
||||
36 + ("Octopuses have three hearts."
|
||||
37 | # Missing comma here.
|
||||
- "Honey never spoils.",
|
||||
38 + "Honey never spoils."),
|
||||
39 | ]
|
||||
40 |
|
||||
41 | facts = {
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ISC004 [*] Unparenthesized implicit string concatenation in collection
|
||||
--> ISC004.py:42:5
|
||||
|
|
||||
41 | facts = {
|
||||
42 | / "Octopuses have three hearts."
|
||||
43 | | # Missing comma here.
|
||||
44 | | "Honey never spoils.",
|
||||
| |_________________________^
|
||||
45 | }
|
||||
|
|
||||
help: Wrap implicitly concatenated strings in parentheses
|
||||
help: Did you forget a comma?
|
||||
39 | ]
|
||||
40 |
|
||||
41 | facts = {
|
||||
- "Octopuses have three hearts."
|
||||
42 + ("Octopuses have three hearts."
|
||||
43 | # Missing comma here.
|
||||
- "Honey never spoils.",
|
||||
44 + "Honey never spoils."),
|
||||
45 | }
|
||||
46 |
|
||||
47 | facts = (
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -210,6 +210,7 @@ pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, positio
|
||||
|
||||
/// Returns `true` if the given call is a top-level expression in its statement.
|
||||
/// This means the call's return value is not used, so return type changes don't matter.
|
||||
pub(crate) fn is_top_level_expression_call(checker: &Checker) -> bool {
|
||||
pub(crate) fn is_top_level_expression_in_statement(checker: &Checker) -> bool {
|
||||
checker.semantic().current_expression_parent().is_none()
|
||||
&& checker.semantic().current_statement().is_expr_stmt()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::preview::is_fix_os_getcwd_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_call;
|
||||
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_in_statement;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -89,7 +89,7 @@ pub(crate) fn os_getcwd(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
|
||||
// Unsafe when the fix would delete comments or change a used return value
|
||||
let applicability = if checker.comment_ranges().intersects(range)
|
||||
|| !is_top_level_expression_call(checker)
|
||||
|| !is_top_level_expression_in_statement(checker)
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_readlink_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
|
||||
is_top_level_expression_call,
|
||||
is_top_level_expression_in_statement,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
return;
|
||||
}
|
||||
|
||||
let applicability = if !is_top_level_expression_call(checker) {
|
||||
let applicability = if !is_top_level_expression_in_statement(checker) {
|
||||
// Unsafe because the return type changes (str/bytes -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_rename_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_call,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_in_statement,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
@@ -92,7 +92,7 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
);
|
||||
|
||||
// Unsafe when the fix would delete comments or change a used return value
|
||||
let applicability = if !is_top_level_expression_call(checker) {
|
||||
let applicability = if !is_top_level_expression_in_statement(checker) {
|
||||
// Unsafe because the return type changes (None -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_replace_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_call,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_in_statement,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
@@ -95,7 +95,7 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
);
|
||||
|
||||
// Unsafe when the fix would delete comments or change a used return value
|
||||
let applicability = if !is_top_level_expression_call(checker) {
|
||||
let applicability = if !is_top_level_expression_in_statement(checker) {
|
||||
// Unsafe because the return type changes (None -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -567,5 +567,64 @@ PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
||||
138 |
|
||||
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
|
|
||||
help: Replace with `Path(...).samefile()`
|
||||
|
||||
PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
--> full_name.py:144:4
|
||||
|
|
||||
142 | import sys
|
||||
143 |
|
||||
144 | if os.rename("pth1.py", "pth1.py.bak"):
|
||||
| ^^^^^^^^^
|
||||
145 | print("rename: truthy")
|
||||
146 | else:
|
||||
|
|
||||
help: Replace with `Path(...).rename(...)`
|
||||
|
||||
PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
--> full_name.py:149:4
|
||||
|
|
||||
147 | print("rename: falsey")
|
||||
148 |
|
||||
149 | if os.replace("pth1.py.bak", "pth1.py"):
|
||||
| ^^^^^^^^^^
|
||||
150 | print("replace: truthy")
|
||||
151 | else:
|
||||
|
|
||||
help: Replace with `Path(...).replace(...)`
|
||||
|
||||
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
--> full_name.py:155:14
|
||||
|
|
||||
154 | try:
|
||||
155 | for _ in os.getcwd():
|
||||
| ^^^^^^^^^
|
||||
156 | print("getcwd: iterable")
|
||||
157 | break
|
||||
|
|
||||
help: Replace with `Path.cwd()`
|
||||
|
||||
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
--> full_name.py:162:14
|
||||
|
|
||||
161 | try:
|
||||
162 | for _ in os.getcwdb():
|
||||
| ^^^^^^^^^^
|
||||
163 | print("getcwdb: iterable")
|
||||
164 | break
|
||||
|
|
||||
help: Replace with `Path.cwd()`
|
||||
|
||||
PTH115 `os.readlink()` should be replaced by `Path.readlink()`
|
||||
--> full_name.py:169:14
|
||||
|
|
||||
168 | try:
|
||||
169 | for _ in os.readlink(sys.executable):
|
||||
| ^^^^^^^^^^^
|
||||
170 | print("readlink: iterable")
|
||||
171 | break
|
||||
|
|
||||
help: Replace with `Path(...).readlink()`
|
||||
|
||||
@@ -1037,5 +1037,142 @@ PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
||||
138 |
|
||||
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
|
|
||||
help: Replace with `Path(...).samefile()`
|
||||
|
||||
PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
--> full_name.py:144:4
|
||||
|
|
||||
142 | import sys
|
||||
143 |
|
||||
144 | if os.rename("pth1.py", "pth1.py.bak"):
|
||||
| ^^^^^^^^^
|
||||
145 | print("rename: truthy")
|
||||
146 | else:
|
||||
|
|
||||
help: Replace with `Path(...).rename(...)`
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
142 | import sys
|
||||
143 + import pathlib
|
||||
144 |
|
||||
- if os.rename("pth1.py", "pth1.py.bak"):
|
||||
145 + if pathlib.Path("pth1.py").rename("pth1.py.bak"):
|
||||
146 | print("rename: truthy")
|
||||
147 | else:
|
||||
148 | print("rename: falsey")
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PTH105 [*] `os.replace()` should be replaced by `Path.replace()`
|
||||
--> full_name.py:149:4
|
||||
|
|
||||
147 | print("rename: falsey")
|
||||
148 |
|
||||
149 | if os.replace("pth1.py.bak", "pth1.py"):
|
||||
| ^^^^^^^^^^
|
||||
150 | print("replace: truthy")
|
||||
151 | else:
|
||||
|
|
||||
help: Replace with `Path(...).replace(...)`
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
142 | import sys
|
||||
143 + import pathlib
|
||||
144 |
|
||||
145 | if os.rename("pth1.py", "pth1.py.bak"):
|
||||
146 | print("rename: truthy")
|
||||
147 | else:
|
||||
148 | print("rename: falsey")
|
||||
149 |
|
||||
- if os.replace("pth1.py.bak", "pth1.py"):
|
||||
150 + if pathlib.Path("pth1.py.bak").replace("pth1.py"):
|
||||
151 | print("replace: truthy")
|
||||
152 | else:
|
||||
153 | print("replace: falsey")
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
--> full_name.py:155:14
|
||||
|
|
||||
154 | try:
|
||||
155 | for _ in os.getcwd():
|
||||
| ^^^^^^^^^
|
||||
156 | print("getcwd: iterable")
|
||||
157 | break
|
||||
|
|
||||
help: Replace with `Path.cwd()`
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
142 | import sys
|
||||
143 + import pathlib
|
||||
144 |
|
||||
145 | if os.rename("pth1.py", "pth1.py.bak"):
|
||||
146 | print("rename: truthy")
|
||||
--------------------------------------------------------------------------------
|
||||
153 | print("replace: falsey")
|
||||
154 |
|
||||
155 | try:
|
||||
- for _ in os.getcwd():
|
||||
156 + for _ in pathlib.Path.cwd():
|
||||
157 | print("getcwd: iterable")
|
||||
158 | break
|
||||
159 | except TypeError as e:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
|
||||
--> full_name.py:162:14
|
||||
|
|
||||
161 | try:
|
||||
162 | for _ in os.getcwdb():
|
||||
| ^^^^^^^^^^
|
||||
163 | print("getcwdb: iterable")
|
||||
164 | break
|
||||
|
|
||||
help: Replace with `Path.cwd()`
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
142 | import sys
|
||||
143 + import pathlib
|
||||
144 |
|
||||
145 | if os.rename("pth1.py", "pth1.py.bak"):
|
||||
146 | print("rename: truthy")
|
||||
--------------------------------------------------------------------------------
|
||||
160 | print("getcwd: not iterable")
|
||||
161 |
|
||||
162 | try:
|
||||
- for _ in os.getcwdb():
|
||||
163 + for _ in pathlib.Path.cwd():
|
||||
164 | print("getcwdb: iterable")
|
||||
165 | break
|
||||
166 | except TypeError as e:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
|
||||
--> full_name.py:169:14
|
||||
|
|
||||
168 | try:
|
||||
169 | for _ in os.readlink(sys.executable):
|
||||
| ^^^^^^^^^^^
|
||||
170 | print("readlink: iterable")
|
||||
171 | break
|
||||
|
|
||||
help: Replace with `Path(...).readlink()`
|
||||
140 |
|
||||
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
||||
142 | import sys
|
||||
143 + import pathlib
|
||||
144 |
|
||||
145 | if os.rename("pth1.py", "pth1.py.bak"):
|
||||
146 | print("rename: truthy")
|
||||
--------------------------------------------------------------------------------
|
||||
167 | print("getcwdb: not iterable")
|
||||
168 |
|
||||
169 | try:
|
||||
- for _ in os.readlink(sys.executable):
|
||||
170 + for _ in pathlib.Path(sys.executable).readlink():
|
||||
171 | print("readlink: iterable")
|
||||
172 | break
|
||||
173 | except TypeError as e:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -902,56 +902,76 @@ help: Convert to f-string
|
||||
132 | # Non-errors
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:160:1
|
||||
--> UP032_0.py:135:1
|
||||
|
|
||||
158 | r'"\N{snowman} {}".format(a)'
|
||||
159 |
|
||||
160 | / "123456789 {}".format(
|
||||
161 | | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
162 | | )
|
||||
| |_^
|
||||
163 |
|
||||
164 | """
|
||||
133 | ###
|
||||
134 |
|
||||
135 | "\N{snowman} {}".format(a)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
136 |
|
||||
137 | "{".format(a)
|
||||
|
|
||||
help: Convert to f-string
|
||||
157 |
|
||||
158 | r'"\N{snowman} {}".format(a)'
|
||||
159 |
|
||||
132 | # Non-errors
|
||||
133 | ###
|
||||
134 |
|
||||
- "\N{snowman} {}".format(a)
|
||||
135 + f"\N{snowman} {a}"
|
||||
136 |
|
||||
137 | "{".format(a)
|
||||
138 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:159:1
|
||||
|
|
||||
157 | r'"\N{snowman} {}".format(a)'
|
||||
158 |
|
||||
159 | / "123456789 {}".format(
|
||||
160 | | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
161 | | )
|
||||
| |_^
|
||||
162 |
|
||||
163 | """
|
||||
|
|
||||
help: Convert to f-string
|
||||
156 |
|
||||
157 | r'"\N{snowman} {}".format(a)'
|
||||
158 |
|
||||
- "123456789 {}".format(
|
||||
- 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
- )
|
||||
160 + f"123456789 {11111111111111111111111111111111111111111111111111111111111111111111111111}"
|
||||
161 |
|
||||
162 | """
|
||||
163 | {}
|
||||
159 + f"123456789 {11111111111111111111111111111111111111111111111111111111111111111111111111}"
|
||||
160 |
|
||||
161 | """
|
||||
162 | {}
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:164:1
|
||||
--> UP032_0.py:163:1
|
||||
|
|
||||
162 | )
|
||||
163 |
|
||||
164 | / """
|
||||
161 | )
|
||||
162 |
|
||||
163 | / """
|
||||
164 | | {}
|
||||
165 | | {}
|
||||
166 | | {}
|
||||
167 | | {}
|
||||
168 | | """.format(
|
||||
169 | | 1,
|
||||
170 | | 2,
|
||||
171 | | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
172 | | )
|
||||
167 | | """.format(
|
||||
168 | | 1,
|
||||
169 | | 2,
|
||||
170 | | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
171 | | )
|
||||
| |_^
|
||||
173 |
|
||||
174 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
172 |
|
||||
173 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
|
|
||||
help: Convert to f-string
|
||||
161 | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
162 | )
|
||||
163 |
|
||||
164 + f"""
|
||||
165 + {1}
|
||||
166 + {2}
|
||||
167 + {111111111111111111111111111111111111111111111111111111111111111111111111111111111111111}
|
||||
168 | """
|
||||
160 | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
161 | )
|
||||
162 |
|
||||
163 + f"""
|
||||
164 + {1}
|
||||
165 + {2}
|
||||
166 + {111111111111111111111111111111111111111111111111111111111111111111111111111111111111111}
|
||||
167 | """
|
||||
- {}
|
||||
- {}
|
||||
- {}
|
||||
@@ -960,392 +980,408 @@ help: Convert to f-string
|
||||
- 2,
|
||||
- 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
- )
|
||||
169 |
|
||||
170 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
171 | """.format(
|
||||
168 |
|
||||
169 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
170 | """.format(
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:174:84
|
||||
--> UP032_0.py:173:84
|
||||
|
|
||||
172 | )
|
||||
173 |
|
||||
174 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
171 | )
|
||||
172 |
|
||||
173 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
| ____________________________________________________________________________________^
|
||||
175 | | """.format(
|
||||
176 | | 111111
|
||||
177 | | )
|
||||
174 | | """.format(
|
||||
175 | | 111111
|
||||
176 | | )
|
||||
| |_^
|
||||
178 |
|
||||
179 | "{}".format(
|
||||
177 |
|
||||
178 | "{}".format(
|
||||
|
|
||||
help: Convert to f-string
|
||||
171 | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
172 | )
|
||||
173 |
|
||||
170 | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
171 | )
|
||||
172 |
|
||||
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
- """.format(
|
||||
- 111111
|
||||
- )
|
||||
174 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = f"""{111111}
|
||||
175 + """
|
||||
176 |
|
||||
177 | "{}".format(
|
||||
178 | [
|
||||
173 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = f"""{111111}
|
||||
174 + """
|
||||
175 |
|
||||
176 | "{}".format(
|
||||
177 | [
|
||||
|
||||
UP032 Use f-string instead of `format` call
|
||||
--> UP032_0.py:202:1
|
||||
--> UP032_0.py:201:1
|
||||
|
|
||||
200 | "{}".format(**c)
|
||||
201 |
|
||||
202 | / "{}".format(
|
||||
203 | | 1 # comment
|
||||
204 | | )
|
||||
199 | "{}".format(**c)
|
||||
200 |
|
||||
201 | / "{}".format(
|
||||
202 | | 1 # comment
|
||||
203 | | )
|
||||
| |_^
|
||||
|
|
||||
help: Convert to f-string
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:209:1
|
||||
--> UP032_0.py:208:1
|
||||
|
|
||||
207 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
208 | # existing line length, so it's fine.
|
||||
209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
206 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
207 | # existing line length, so it's fine.
|
||||
208 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
209 |
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
|
|
||||
help: Convert to f-string
|
||||
206 |
|
||||
207 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
208 | # existing line length, so it's fine.
|
||||
205 |
|
||||
206 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
207 | # existing line length, so it's fine.
|
||||
- "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
209 + f"<Customer: {self.internal_ids}, {self.external_ids}, {self.properties}, {self.tags}, {self.others}>"
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
208 + f"<Customer: {self.internal_ids}, {self.external_ids}, {self.properties}, {self.tags}, {self.others}>"
|
||||
209 |
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
211 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:212:18
|
||||
--> UP032_0.py:211:18
|
||||
|
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
211 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
213 | | "".format(new_dict, d))
|
||||
212 | | "".format(new_dict, d))
|
||||
| |_______________________________________^
|
||||
214 |
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
213 |
|
||||
214 | # When fixing, trim the trailing empty string.
|
||||
|
|
||||
help: Convert to f-string
|
||||
209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
208 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
209 |
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
- raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
- "".format(new_dict, d))
|
||||
212 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
|
||||
211 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
|
||||
212 |
|
||||
213 | # When fixing, trim the trailing empty string.
|
||||
214 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:215:18
|
||||
|
|
||||
214 | # When fixing, trim the trailing empty string.
|
||||
215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
216 | | .format(new_dict, d))
|
||||
| |_____________________________________^
|
||||
217 |
|
||||
218 | raise ValueError(
|
||||
|
|
||||
help: Convert to f-string
|
||||
212 | "".format(new_dict, d))
|
||||
213 |
|
||||
214 | # When fixing, trim the trailing empty string.
|
||||
215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:216:18
|
||||
|
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
217 | | .format(new_dict, d))
|
||||
| |_____________________________________^
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
|
|
||||
help: Convert to f-string
|
||||
213 | "".format(new_dict, d))
|
||||
214 |
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
- raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
- .format(new_dict, d))
|
||||
216 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
217 + )
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
220 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
215 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
216 + )
|
||||
217 |
|
||||
218 | raise ValueError(
|
||||
219 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:220:5
|
||||
--> UP032_0.py:219:5
|
||||
|
|
||||
219 | raise ValueError(
|
||||
220 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
221 | | "".format(new_dict, d)
|
||||
218 | raise ValueError(
|
||||
219 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
220 | | "".format(new_dict, d)
|
||||
| |__________________________^
|
||||
222 | )
|
||||
221 | )
|
||||
|
|
||||
help: Convert to f-string
|
||||
217 | .format(new_dict, d))
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
216 | .format(new_dict, d))
|
||||
217 |
|
||||
218 | raise ValueError(
|
||||
- "Conflicting configuration dicts: {!r} {!r}"
|
||||
- "".format(new_dict, d)
|
||||
220 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
219 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
220 | )
|
||||
221 |
|
||||
222 | raise ValueError(
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:224:5
|
||||
|
|
||||
223 | raise ValueError(
|
||||
224 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
225 | | "".format(new_dict, d)
|
||||
| |__________________________^
|
||||
226 |
|
||||
227 | )
|
||||
|
|
||||
help: Convert to f-string
|
||||
221 | )
|
||||
222 |
|
||||
223 | raise ValueError(
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:225:5
|
||||
|
|
||||
224 | raise ValueError(
|
||||
225 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
226 | | "".format(new_dict, d)
|
||||
| |__________________________^
|
||||
227 |
|
||||
228 | )
|
||||
|
|
||||
help: Convert to f-string
|
||||
222 | )
|
||||
223 |
|
||||
224 | raise ValueError(
|
||||
- "Conflicting configuration dicts: {!r} {!r}"
|
||||
- "".format(new_dict, d)
|
||||
225 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
226 |
|
||||
227 | )
|
||||
228 |
|
||||
224 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
225 |
|
||||
226 | )
|
||||
227 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:231:1
|
||||
--> UP032_0.py:230:1
|
||||
|
|
||||
230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
231 | / (
|
||||
232 | | "{}"
|
||||
233 | | "{{}}"
|
||||
234 | | ).format(a)
|
||||
229 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
230 | / (
|
||||
231 | | "{}"
|
||||
232 | | "{{}}"
|
||||
233 | | ).format(a)
|
||||
| |___________^
|
||||
235 |
|
||||
236 | ("{}" "{{}}").format(a)
|
||||
234 |
|
||||
235 | ("{}" "{{}}").format(a)
|
||||
|
|
||||
help: Convert to f-string
|
||||
229 |
|
||||
230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
231 | (
|
||||
232 + f"{a}"
|
||||
233 | "{}"
|
||||
228 |
|
||||
229 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
230 | (
|
||||
231 + f"{a}"
|
||||
232 | "{}"
|
||||
- "{{}}"
|
||||
- ).format(a)
|
||||
234 + )
|
||||
235 |
|
||||
236 | ("{}" "{{}}").format(a)
|
||||
237 |
|
||||
233 + )
|
||||
234 |
|
||||
235 | ("{}" "{{}}").format(a)
|
||||
236 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:236:1
|
||||
--> UP032_0.py:235:1
|
||||
|
|
||||
234 | ).format(a)
|
||||
235 |
|
||||
236 | ("{}" "{{}}").format(a)
|
||||
233 | ).format(a)
|
||||
234 |
|
||||
235 | ("{}" "{{}}").format(a)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to f-string
|
||||
233 | "{{}}"
|
||||
234 | ).format(a)
|
||||
235 |
|
||||
232 | "{{}}"
|
||||
233 | ).format(a)
|
||||
234 |
|
||||
- ("{}" "{{}}").format(a)
|
||||
236 + (f"{a}" "{}")
|
||||
235 + (f"{a}" "{}")
|
||||
236 |
|
||||
237 |
|
||||
238 |
|
||||
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
238 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:240:1
|
||||
--> UP032_0.py:239:1
|
||||
|
|
||||
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
240 | / (
|
||||
241 | | "{}"
|
||||
242 | | "{{{}}}"
|
||||
243 | | ).format(a, b)
|
||||
238 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
239 | / (
|
||||
240 | | "{}"
|
||||
241 | | "{{{}}}"
|
||||
242 | | ).format(a, b)
|
||||
| |______________^
|
||||
244 |
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
243 |
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
|
|
||||
help: Convert to f-string
|
||||
238 |
|
||||
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
240 | (
|
||||
237 |
|
||||
238 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
239 | (
|
||||
- "{}"
|
||||
- "{{{}}}"
|
||||
- ).format(a, b)
|
||||
241 + f"{a}"
|
||||
242 + f"{{{b}}}"
|
||||
243 + )
|
||||
244 |
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
246 |
|
||||
240 + f"{a}"
|
||||
241 + f"{{{b}}}"
|
||||
242 + )
|
||||
243 |
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
245 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:245:1
|
||||
--> UP032_0.py:244:1
|
||||
|
|
||||
243 | ).format(a, b)
|
||||
244 |
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
242 | ).format(a, b)
|
||||
243 |
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
246 |
|
||||
247 | # The dictionary should be parenthesized.
|
||||
245 |
|
||||
246 | # The dictionary should be parenthesized.
|
||||
|
|
||||
help: Convert to f-string
|
||||
242 | "{{{}}}"
|
||||
243 | ).format(a, b)
|
||||
244 |
|
||||
241 | "{{{}}}"
|
||||
242 | ).format(a, b)
|
||||
243 |
|
||||
- ("{}" "{{{}}}").format(a, b)
|
||||
245 + (f"{a}" f"{{{b}}}")
|
||||
246 |
|
||||
247 | # The dictionary should be parenthesized.
|
||||
248 | "{}".format({0: 1}[0])
|
||||
244 + (f"{a}" f"{{{b}}}")
|
||||
245 |
|
||||
246 | # The dictionary should be parenthesized.
|
||||
247 | "{}".format({0: 1}[0])
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:248:1
|
||||
--> UP032_0.py:247:1
|
||||
|
|
||||
247 | # The dictionary should be parenthesized.
|
||||
248 | "{}".format({0: 1}[0])
|
||||
246 | # The dictionary should be parenthesized.
|
||||
247 | "{}".format({0: 1}[0])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
249 |
|
||||
250 | # The dictionary should be parenthesized.
|
||||
248 |
|
||||
249 | # The dictionary should be parenthesized.
|
||||
|
|
||||
help: Convert to f-string
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
246 |
|
||||
247 | # The dictionary should be parenthesized.
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
245 |
|
||||
246 | # The dictionary should be parenthesized.
|
||||
- "{}".format({0: 1}[0])
|
||||
248 + f"{({0: 1}[0])}"
|
||||
249 |
|
||||
250 | # The dictionary should be parenthesized.
|
||||
251 | "{}".format({0: 1}.bar)
|
||||
247 + f"{({0: 1}[0])}"
|
||||
248 |
|
||||
249 | # The dictionary should be parenthesized.
|
||||
250 | "{}".format({0: 1}.bar)
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:251:1
|
||||
--> UP032_0.py:250:1
|
||||
|
|
||||
250 | # The dictionary should be parenthesized.
|
||||
251 | "{}".format({0: 1}.bar)
|
||||
249 | # The dictionary should be parenthesized.
|
||||
250 | "{}".format({0: 1}.bar)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
252 |
|
||||
253 | # The dictionary should be parenthesized.
|
||||
251 |
|
||||
252 | # The dictionary should be parenthesized.
|
||||
|
|
||||
help: Convert to f-string
|
||||
248 | "{}".format({0: 1}[0])
|
||||
249 |
|
||||
250 | # The dictionary should be parenthesized.
|
||||
247 | "{}".format({0: 1}[0])
|
||||
248 |
|
||||
249 | # The dictionary should be parenthesized.
|
||||
- "{}".format({0: 1}.bar)
|
||||
251 + f"{({0: 1}.bar)}"
|
||||
252 |
|
||||
253 | # The dictionary should be parenthesized.
|
||||
254 | "{}".format({0: 1}())
|
||||
250 + f"{({0: 1}.bar)}"
|
||||
251 |
|
||||
252 | # The dictionary should be parenthesized.
|
||||
253 | "{}".format({0: 1}())
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:254:1
|
||||
--> UP032_0.py:253:1
|
||||
|
|
||||
253 | # The dictionary should be parenthesized.
|
||||
254 | "{}".format({0: 1}())
|
||||
252 | # The dictionary should be parenthesized.
|
||||
253 | "{}".format({0: 1}())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
255 |
|
||||
256 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
254 |
|
||||
255 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
|
|
||||
help: Convert to f-string
|
||||
251 | "{}".format({0: 1}.bar)
|
||||
252 |
|
||||
253 | # The dictionary should be parenthesized.
|
||||
250 | "{}".format({0: 1}.bar)
|
||||
251 |
|
||||
252 | # The dictionary should be parenthesized.
|
||||
- "{}".format({0: 1}())
|
||||
254 + f"{({0: 1}())}"
|
||||
255 |
|
||||
256 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
257 | "{x} {x}".format(x=foo())
|
||||
253 + f"{({0: 1}())}"
|
||||
254 |
|
||||
255 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
256 | "{x} {x}".format(x=foo())
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:261:1
|
||||
--> UP032_0.py:260:1
|
||||
|
|
||||
260 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
261 | "{0} {1}".format(foo(), foo())
|
||||
259 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
260 | "{0} {1}".format(foo(), foo())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
262 |
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
261 |
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
|
|
||||
help: Convert to f-string
|
||||
258 | "{0} {0}".format(foo())
|
||||
259 |
|
||||
260 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
257 | "{0} {0}".format(foo())
|
||||
258 |
|
||||
259 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
- "{0} {1}".format(foo(), foo())
|
||||
261 + f"{foo()} {foo()}"
|
||||
262 |
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
264 | ''.format(self.project)
|
||||
260 + f"{foo()} {foo()}"
|
||||
261 |
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
263 | ''.format(self.project)
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:264:1
|
||||
--> UP032_0.py:263:1
|
||||
|
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
264 | ''.format(self.project)
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
263 | ''.format(self.project)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
265 |
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
264 |
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
|
|
||||
help: Convert to f-string
|
||||
261 | "{0} {1}".format(foo(), foo())
|
||||
262 |
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
260 | "{0} {1}".format(foo(), foo())
|
||||
261 |
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
- ''.format(self.project)
|
||||
264 + ''
|
||||
265 |
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
267 | "".format(self.project)
|
||||
263 + ''
|
||||
264 |
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
266 | "".format(self.project)
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:267:1
|
||||
--> UP032_0.py:266:1
|
||||
|
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
267 | "".format(self.project)
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
266 | "".format(self.project)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
268 |
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
267 |
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
|
|
||||
help: Convert to f-string
|
||||
264 | ''.format(self.project)
|
||||
265 |
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
263 | ''.format(self.project)
|
||||
264 |
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
- "".format(self.project)
|
||||
267 + ""
|
||||
268 |
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
266 + ""
|
||||
267 |
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
269 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:271:5
|
||||
--> UP032_0.py:270:5
|
||||
|
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
271 | x: "'{} + {}'.format(x, y)"
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
269 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
270 | x: "'{} + {}'.format(x, y)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
272 |
|
||||
273 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
271 |
|
||||
272 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
|
|
||||
help: Convert to f-string
|
||||
268 |
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
267 |
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
269 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
- x: "'{} + {}'.format(x, y)"
|
||||
271 + x: "f'{x} + {y}'"
|
||||
272 |
|
||||
273 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
274 | # Fix should parenthesize walrus
|
||||
270 + x: "f'{x} + {y}'"
|
||||
271 |
|
||||
272 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
273 | # Fix should parenthesize walrus
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:277:14
|
||||
--> UP032_0.py:276:14
|
||||
|
|
||||
275 | if __name__ == "__main__":
|
||||
276 | number = 0
|
||||
277 | string = "{}".format(number := number + 1)
|
||||
274 | if __name__ == "__main__":
|
||||
275 | number = 0
|
||||
276 | string = "{}".format(number := number + 1)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
278 | print(string)
|
||||
277 | print(string)
|
||||
|
|
||||
help: Convert to f-string
|
||||
274 | # Fix should parenthesize walrus
|
||||
275 | if __name__ == "__main__":
|
||||
276 | number = 0
|
||||
273 | # Fix should parenthesize walrus
|
||||
274 | if __name__ == "__main__":
|
||||
275 | number = 0
|
||||
- string = "{}".format(number := number + 1)
|
||||
277 + string = f"{(number := number + 1)}"
|
||||
278 | print(string)
|
||||
276 + string = f"{(number := number + 1)}"
|
||||
277 | print(string)
|
||||
278 |
|
||||
279 | # Unicode escape
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:280:1
|
||||
|
|
||||
279 | # Unicode escape
|
||||
280 | "\N{angle}AOB = {angle}°".format(angle=180)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to f-string
|
||||
277 | print(string)
|
||||
278 |
|
||||
279 | # Unicode escape
|
||||
- "\N{angle}AOB = {angle}°".format(angle=180)
|
||||
280 + f"\N{angle}AOB = {180}°"
|
||||
|
||||
@@ -3,10 +3,11 @@ use std::borrow::Cow;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
|
||||
use ruff_python_semantic::{ResolvedReference, SemanticModel};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_async::rules::blocking_open_call::is_open_call_from_pathlib;
|
||||
use crate::{Applicability, Edit, Fix};
|
||||
|
||||
/// Format a code snippet to call `name.method()`.
|
||||
@@ -119,14 +120,13 @@ impl OpenMode {
|
||||
pub(super) struct FileOpen<'a> {
|
||||
/// With item where the open happens, we use it for the reporting range.
|
||||
pub(super) item: &'a ast::WithItem,
|
||||
/// Filename expression used as the first argument in `open`, we use it in the diagnostic message.
|
||||
pub(super) filename: &'a Expr,
|
||||
/// The file open mode.
|
||||
pub(super) mode: OpenMode,
|
||||
/// The file open keywords.
|
||||
pub(super) keywords: Vec<&'a ast::Keyword>,
|
||||
/// We only check `open` operations whose file handles are used exactly once.
|
||||
pub(super) reference: &'a ResolvedReference,
|
||||
pub(super) argument: OpenArgument<'a>,
|
||||
}
|
||||
|
||||
impl FileOpen<'_> {
|
||||
@@ -137,6 +137,45 @@ impl FileOpen<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) enum OpenArgument<'a> {
|
||||
/// The filename argument to `open`, e.g. "foo.txt" in:
|
||||
///
|
||||
/// ```py
|
||||
/// f = open("foo.txt")
|
||||
/// ```
|
||||
Builtin { filename: &'a Expr },
|
||||
/// The `Path` receiver of a `pathlib.Path.open` call, e.g. the `p` in the
|
||||
/// context manager in:
|
||||
///
|
||||
/// ```py
|
||||
/// p = Path("foo.txt")
|
||||
/// with p.open() as f: ...
|
||||
/// ```
|
||||
///
|
||||
/// or `Path("foo.txt")` in
|
||||
///
|
||||
/// ```py
|
||||
/// with Path("foo.txt").open() as f: ...
|
||||
/// ```
|
||||
Pathlib { path: &'a Expr },
|
||||
}
|
||||
|
||||
impl OpenArgument<'_> {
|
||||
pub(super) fn display<'src>(&self, source: &'src str) -> &'src str {
|
||||
&source[self.range()]
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for OpenArgument<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
OpenArgument::Builtin { filename } => filename.range(),
|
||||
OpenArgument::Pathlib { path } => path.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find and return all `open` operations in the given `with` statement.
|
||||
pub(super) fn find_file_opens<'a>(
|
||||
with: &'a ast::StmtWith,
|
||||
@@ -146,10 +185,65 @@ pub(super) fn find_file_opens<'a>(
|
||||
) -> Vec<FileOpen<'a>> {
|
||||
with.items
|
||||
.iter()
|
||||
.filter_map(|item| find_file_open(item, with, semantic, read_mode, python_version))
|
||||
.filter_map(|item| {
|
||||
find_file_open(item, with, semantic, read_mode, python_version)
|
||||
.or_else(|| find_path_open(item, with, semantic, read_mode, python_version))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_file_open<'a>(
|
||||
item: &'a ast::WithItem,
|
||||
with: &'a ast::StmtWith,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
read_mode: bool,
|
||||
mode: OpenMode,
|
||||
keywords: Vec<&'a ast::Keyword>,
|
||||
argument: OpenArgument<'a>,
|
||||
) -> Option<FileOpen<'a>> {
|
||||
match mode {
|
||||
OpenMode::ReadText | OpenMode::ReadBytes => {
|
||||
if !read_mode {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
OpenMode::WriteText | OpenMode::WriteBytes => {
|
||||
if read_mode {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let var = item.optional_vars.as_deref()?.as_name_expr()?;
|
||||
let scope = semantic.current_scope();
|
||||
|
||||
let binding = scope.get_all(var.id.as_str()).find_map(|id| {
|
||||
let b = semantic.binding(id);
|
||||
(b.range() == var.range()).then_some(b)
|
||||
})?;
|
||||
let references: Vec<&ResolvedReference> = binding
|
||||
.references
|
||||
.iter()
|
||||
.map(|id| semantic.reference(*id))
|
||||
.filter(|reference| with.range().contains_range(reference.range()))
|
||||
.collect();
|
||||
|
||||
let [reference] = references.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(FileOpen {
|
||||
item,
|
||||
mode,
|
||||
keywords,
|
||||
reference,
|
||||
argument,
|
||||
})
|
||||
}
|
||||
|
||||
/// Find `open` operation in the given `with` item.
|
||||
fn find_file_open<'a>(
|
||||
item: &'a ast::WithItem,
|
||||
@@ -165,8 +259,6 @@ fn find_file_open<'a>(
|
||||
..
|
||||
} = item.context_expr.as_call_expr()?;
|
||||
|
||||
let var = item.optional_vars.as_deref()?.as_name_expr()?;
|
||||
|
||||
// Ignore calls with `*args` and `**kwargs`. In the exact case of `open(*filename, mode="w")`,
|
||||
// it could be a match; but in all other cases, the call _could_ contain unsupported keyword
|
||||
// arguments, like `buffering`.
|
||||
@@ -187,58 +279,57 @@ fn find_file_open<'a>(
|
||||
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
|
||||
|
||||
let mode = kw_mode.unwrap_or(pos_mode);
|
||||
|
||||
match mode {
|
||||
OpenMode::ReadText | OpenMode::ReadBytes => {
|
||||
if !read_mode {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
OpenMode::WriteText | OpenMode::WriteBytes => {
|
||||
if read_mode {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Path.read_bytes and Path.write_bytes do not support any kwargs.
|
||||
if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Now we need to find what is this variable bound to...
|
||||
let scope = semantic.current_scope();
|
||||
let bindings: Vec<BindingId> = scope.get_all(var.id.as_str()).collect();
|
||||
|
||||
let binding = bindings
|
||||
.iter()
|
||||
.map(|id| semantic.binding(*id))
|
||||
// We might have many bindings with the same name, but we only care
|
||||
// for the one we are looking at right now.
|
||||
.find(|binding| binding.range() == var.range())?;
|
||||
|
||||
// Since many references can share the same binding, we can limit our attention span
|
||||
// exclusively to the body of the current `with` statement.
|
||||
let references: Vec<&ResolvedReference> = binding
|
||||
.references
|
||||
.iter()
|
||||
.map(|id| semantic.reference(*id))
|
||||
.filter(|reference| with.range().contains_range(reference.range()))
|
||||
.collect();
|
||||
|
||||
// And even with all these restrictions, if the file handle gets used not exactly once,
|
||||
// it doesn't fit the bill.
|
||||
let [reference] = references.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(FileOpen {
|
||||
resolve_file_open(
|
||||
item,
|
||||
filename,
|
||||
with,
|
||||
semantic,
|
||||
read_mode,
|
||||
mode,
|
||||
keywords,
|
||||
reference,
|
||||
})
|
||||
OpenArgument::Builtin { filename },
|
||||
)
|
||||
}
|
||||
|
||||
fn find_path_open<'a>(
|
||||
item: &'a ast::WithItem,
|
||||
with: &'a ast::StmtWith,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
read_mode: bool,
|
||||
python_version: PythonVersion,
|
||||
) -> Option<FileOpen<'a>> {
|
||||
let ast::ExprCall {
|
||||
func,
|
||||
arguments: ast::Arguments { args, keywords, .. },
|
||||
..
|
||||
} = item.context_expr.as_call_expr()?;
|
||||
if args.iter().any(Expr::is_starred_expr)
|
||||
|| keywords.iter().any(|keyword| keyword.arg.is_none())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if !is_open_call_from_pathlib(func, semantic) {
|
||||
return None;
|
||||
}
|
||||
let attr = func.as_attribute_expr()?;
|
||||
let mode = if args.is_empty() {
|
||||
OpenMode::ReadText
|
||||
} else {
|
||||
match_open_mode(args.first()?)?
|
||||
};
|
||||
|
||||
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
|
||||
let mode = kw_mode.unwrap_or(mode);
|
||||
resolve_file_open(
|
||||
item,
|
||||
with,
|
||||
semantic,
|
||||
read_mode,
|
||||
mode,
|
||||
keywords,
|
||||
OpenArgument::Pathlib {
|
||||
path: attr.value.as_ref(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Match positional arguments. Return expression for the file name and open mode.
|
||||
|
||||
@@ -15,7 +15,8 @@ mod tests {
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, settings};
|
||||
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101.py"))]
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101_0.py"))]
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101_1.py"))]
|
||||
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
|
||||
#[test_case(Rule::IfExpInsteadOfOrOperator, Path::new("FURB110.py"))]
|
||||
#[test_case(Rule::ReimplementedOperator, Path::new("FURB118.py"))]
|
||||
@@ -46,7 +47,8 @@ mod tests {
|
||||
#[test_case(Rule::MetaClassABCMeta, Path::new("FURB180.py"))]
|
||||
#[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))]
|
||||
#[test_case(Rule::ListReverseCopy, Path::new("FURB187.py"))]
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))]
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103_0.py"))]
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103_1.py"))]
|
||||
#[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))]
|
||||
#[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))]
|
||||
#[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))]
|
||||
@@ -65,7 +67,7 @@ mod tests {
|
||||
#[test]
|
||||
fn write_whole_file_python_39() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("refurb/FURB103.py"),
|
||||
Path::new("refurb/FURB103_0.py"),
|
||||
&settings::LinterSettings::for_rule(Rule::WriteWholeFile)
|
||||
.with_target_version(PythonVersion::PY39),
|
||||
)?;
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::rules::refurb::helpers::{FileOpen, find_file_opens};
|
||||
use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens};
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -42,27 +42,41 @@ use crate::{FixAvailability, Violation};
|
||||
/// - [Python documentation: `Path.read_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.read_text)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "v0.1.2")]
|
||||
pub(crate) struct ReadWholeFile {
|
||||
pub(crate) struct ReadWholeFile<'a> {
|
||||
filename: SourceCodeSnippet,
|
||||
suggestion: SourceCodeSnippet,
|
||||
argument: OpenArgument<'a>,
|
||||
}
|
||||
|
||||
impl Violation for ReadWholeFile {
|
||||
impl Violation for ReadWholeFile<'_> {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let filename = self.filename.truncated_display();
|
||||
let suggestion = self.suggestion.truncated_display();
|
||||
format!("`open` and `read` should be replaced by `Path({filename}).{suggestion}`")
|
||||
match self.argument {
|
||||
OpenArgument::Pathlib { .. } => {
|
||||
format!(
|
||||
"`Path.open()` followed by `read()` can be replaced by `{filename}.{suggestion}`"
|
||||
)
|
||||
}
|
||||
OpenArgument::Builtin { .. } => {
|
||||
format!("`open` and `read` should be replaced by `Path({filename}).{suggestion}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!(
|
||||
"Replace with `Path({}).{}`",
|
||||
self.filename.truncated_display(),
|
||||
self.suggestion.truncated_display(),
|
||||
))
|
||||
let filename = self.filename.truncated_display();
|
||||
let suggestion = self.suggestion.truncated_display();
|
||||
|
||||
match self.argument {
|
||||
OpenArgument::Pathlib { .. } => Some(format!("Replace with `{filename}.{suggestion}`")),
|
||||
OpenArgument::Builtin { .. } => {
|
||||
Some(format!("Replace with `Path({filename}).{suggestion}`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +128,13 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
|
||||
.position(|open| open.is_ref(read_from))
|
||||
{
|
||||
let open = self.candidates.remove(open);
|
||||
let filename_display = open.argument.display(self.checker.source());
|
||||
let suggestion = make_suggestion(&open, self.checker.generator());
|
||||
let mut diagnostic = self.checker.report_diagnostic(
|
||||
ReadWholeFile {
|
||||
filename: SourceCodeSnippet::from_str(
|
||||
&self.checker.generator().expr(open.filename),
|
||||
),
|
||||
filename: SourceCodeSnippet::from_str(filename_display),
|
||||
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
||||
argument: open.argument,
|
||||
},
|
||||
open.item.range(),
|
||||
);
|
||||
@@ -188,8 +202,6 @@ fn generate_fix(
|
||||
|
||||
let locator = checker.locator();
|
||||
|
||||
let filename_code = locator.slice(open.filename.range());
|
||||
|
||||
let (import_edit, binding) = checker
|
||||
.importer()
|
||||
.get_or_import_symbol(
|
||||
@@ -206,10 +218,15 @@ fn generate_fix(
|
||||
[Stmt::Assign(ast::StmtAssign { targets, value, .. })] if value.range() == expr.range() => {
|
||||
match targets.as_slice() {
|
||||
[Expr::Name(name)] => {
|
||||
format!(
|
||||
"{name} = {binding}({filename_code}).{suggestion}",
|
||||
name = name.id
|
||||
)
|
||||
let target = match open.argument {
|
||||
OpenArgument::Builtin { filename } => {
|
||||
let filename_code = locator.slice(filename.range());
|
||||
format!("{binding}({filename_code})")
|
||||
}
|
||||
OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(),
|
||||
};
|
||||
|
||||
format!("{name} = {target}.{suggestion}", name = name.id)
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
@@ -223,8 +240,16 @@ fn generate_fix(
|
||||
}),
|
||||
] if value.range() == expr.range() => match target.as_ref() {
|
||||
Expr::Name(name) => {
|
||||
let target = match open.argument {
|
||||
OpenArgument::Builtin { filename } => {
|
||||
let filename_code = locator.slice(filename.range());
|
||||
format!("{binding}({filename_code})")
|
||||
}
|
||||
OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(),
|
||||
};
|
||||
|
||||
format!(
|
||||
"{var}: {ann} = {binding}({filename_code}).{suggestion}",
|
||||
"{var}: {ann} = {target}.{suggestion}",
|
||||
var = name.id,
|
||||
ann = locator.slice(annotation.range())
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::rules::refurb::helpers::{FileOpen, find_file_opens};
|
||||
use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens};
|
||||
use crate::{FixAvailability, Locator, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -42,26 +42,40 @@ use crate::{FixAvailability, Locator, Violation};
|
||||
/// - [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "v0.3.6")]
|
||||
pub(crate) struct WriteWholeFile {
|
||||
pub(crate) struct WriteWholeFile<'a> {
|
||||
filename: SourceCodeSnippet,
|
||||
suggestion: SourceCodeSnippet,
|
||||
argument: OpenArgument<'a>,
|
||||
}
|
||||
|
||||
impl Violation for WriteWholeFile {
|
||||
impl Violation for WriteWholeFile<'_> {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let filename = self.filename.truncated_display();
|
||||
let suggestion = self.suggestion.truncated_display();
|
||||
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
|
||||
match self.argument {
|
||||
OpenArgument::Pathlib { .. } => {
|
||||
format!(
|
||||
"`Path.open()` followed by `write()` can be replaced by `{filename}.{suggestion}`"
|
||||
)
|
||||
}
|
||||
OpenArgument::Builtin { .. } => {
|
||||
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!(
|
||||
"Replace with `Path({}).{}`",
|
||||
self.filename.truncated_display(),
|
||||
self.suggestion.truncated_display(),
|
||||
))
|
||||
let filename = self.filename.truncated_display();
|
||||
let suggestion = self.suggestion.truncated_display();
|
||||
|
||||
match self.argument {
|
||||
OpenArgument::Pathlib { .. } => Some(format!("Replace with `{filename}.{suggestion}`")),
|
||||
OpenArgument::Builtin { .. } => {
|
||||
Some(format!("Replace with `Path({filename}).{suggestion}`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +139,15 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
|
||||
.position(|open| open.is_ref(write_to))
|
||||
{
|
||||
let open = self.candidates.remove(open);
|
||||
|
||||
if self.loop_counter == 0 {
|
||||
let filename_display = open.argument.display(self.checker.source());
|
||||
let suggestion = make_suggestion(&open, content, self.checker.locator());
|
||||
|
||||
let mut diagnostic = self.checker.report_diagnostic(
|
||||
WriteWholeFile {
|
||||
filename: SourceCodeSnippet::from_str(
|
||||
&self.checker.generator().expr(open.filename),
|
||||
),
|
||||
filename: SourceCodeSnippet::from_str(filename_display),
|
||||
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
||||
argument: open.argument,
|
||||
},
|
||||
open.item.range(),
|
||||
);
|
||||
@@ -198,7 +211,6 @@ fn generate_fix(
|
||||
}
|
||||
|
||||
let locator = checker.locator();
|
||||
let filename_code = locator.slice(open.filename.range());
|
||||
|
||||
let (import_edit, binding) = checker
|
||||
.importer()
|
||||
@@ -209,7 +221,15 @@ fn generate_fix(
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
let replacement = format!("{binding}({filename_code}).{suggestion}");
|
||||
let target = match open.argument {
|
||||
OpenArgument::Builtin { filename } => {
|
||||
let filename_code = locator.slice(filename.range());
|
||||
format!("{binding}({filename_code})")
|
||||
}
|
||||
OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(),
|
||||
};
|
||||
|
||||
let replacement = format!("{target}.{suggestion}");
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(with_stmt.range()) {
|
||||
Applicability::Unsafe
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text()`
|
||||
--> FURB101.py:12:6
|
||||
--> FURB101_0.py:12:6
|
||||
|
|
||||
11 | # FURB101
|
||||
12 | with open("file.txt") as f:
|
||||
@@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").read_text()`
|
||||
16 | with open("file.txt", "rb") as f:
|
||||
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_bytes()`
|
||||
--> FURB101.py:16:6
|
||||
--> FURB101_0.py:16:6
|
||||
|
|
||||
15 | # FURB101
|
||||
16 | with open("file.txt", "rb") as f:
|
||||
@@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").read_bytes()`
|
||||
20 | with open("file.txt", mode="rb") as f:
|
||||
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_bytes()`
|
||||
--> FURB101.py:20:6
|
||||
--> FURB101_0.py:20:6
|
||||
|
|
||||
19 | # FURB101
|
||||
20 | with open("file.txt", mode="rb") as f:
|
||||
@@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").read_bytes()`
|
||||
24 | with open("file.txt", encoding="utf8") as f:
|
||||
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf8")`
|
||||
--> FURB101.py:24:6
|
||||
--> FURB101_0.py:24:6
|
||||
|
|
||||
23 | # FURB101
|
||||
24 | with open("file.txt", encoding="utf8") as f:
|
||||
@@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").read_text(encoding="utf8")`
|
||||
28 | with open("file.txt", errors="ignore") as f:
|
||||
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(errors="ignore")`
|
||||
--> FURB101.py:28:6
|
||||
--> FURB101_0.py:28:6
|
||||
|
|
||||
27 | # FURB101
|
||||
28 | with open("file.txt", errors="ignore") as f:
|
||||
@@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").read_text(errors="ignore")`
|
||||
32 | with open("file.txt", mode="r") as f: # noqa: FURB120
|
||||
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text()`
|
||||
--> FURB101.py:32:6
|
||||
--> FURB101_0.py:32:6
|
||||
|
|
||||
31 | # FURB101
|
||||
32 | with open("file.txt", mode="r") as f: # noqa: FURB120
|
||||
@@ -147,7 +147,7 @@ help: Replace with `Path("file.txt").read_text()`
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()`
|
||||
--> FURB101.py:36:6
|
||||
--> FURB101_0.py:36:6
|
||||
|
|
||||
35 | # FURB101
|
||||
36 | with open(foo(), "rb") as f:
|
||||
@@ -158,7 +158,7 @@ FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()`
|
||||
help: Replace with `Path(foo()).read_bytes()`
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()`
|
||||
--> FURB101.py:44:6
|
||||
--> FURB101_0.py:44:6
|
||||
|
|
||||
43 | # FURB101
|
||||
44 | with open("a.txt") as a, open("b.txt", "rb") as b:
|
||||
@@ -169,7 +169,7 @@ FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()`
|
||||
help: Replace with `Path("a.txt").read_text()`
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()`
|
||||
--> FURB101.py:44:26
|
||||
--> FURB101_0.py:44:26
|
||||
|
|
||||
43 | # FURB101
|
||||
44 | with open("a.txt") as a, open("b.txt", "rb") as b:
|
||||
@@ -180,7 +180,7 @@ FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()`
|
||||
help: Replace with `Path("b.txt").read_bytes()`
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()`
|
||||
--> FURB101.py:49:18
|
||||
--> FURB101_0.py:49:18
|
||||
|
|
||||
48 | # FURB101
|
||||
49 | with foo() as a, open("file.txt") as b, foo() as c:
|
||||
@@ -191,7 +191,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()`
|
||||
help: Replace with `Path("file.txt").read_text()`
|
||||
|
||||
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101.py:130:6
|
||||
--> FURB101_0.py:130:6
|
||||
|
|
||||
129 | # FURB101
|
||||
130 | with open("file.txt", encoding="utf-8") as f:
|
||||
@@ -215,7 +215,7 @@ help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
|
||||
134 | with open("file.txt", encoding="utf-8") as f:
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101.py:134:6
|
||||
--> FURB101_0.py:134:6
|
||||
|
|
||||
133 | # FURB101 but no fix because it would remove the assignment to `x`
|
||||
134 | with open("file.txt", encoding="utf-8") as f:
|
||||
@@ -225,7 +225,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(enco
|
||||
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101.py:138:6
|
||||
--> FURB101_0.py:138:6
|
||||
|
|
||||
137 | # FURB101 but no fix because it would remove the `process_contents` call
|
||||
138 | with open("file.txt", encoding="utf-8") as f:
|
||||
@@ -234,13 +234,13 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(enco
|
||||
|
|
||||
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
|
||||
|
||||
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101.py:141:6
|
||||
FURB101 `open` and `read` should be replaced by `Path("file1.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101_0.py:141:6
|
||||
|
|
||||
139 | contents = process_contents(f.read())
|
||||
140 |
|
||||
141 | with open("file.txt", encoding="utf-8") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
141 | with open("file1.txt", encoding="utf-8") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
142 | contents: str = process_contents(f.read())
|
||||
|
|
||||
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
|
||||
help: Replace with `Path("file1.txt").read_text(encoding="utf-8")`
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB101 [*] `Path.open()` followed by `read()` can be replaced by `Path("file.txt").read_text()`
|
||||
--> FURB101_1.py:4:6
|
||||
|
|
||||
2 | from pathlib import Path
|
||||
3 |
|
||||
4 | with Path("file.txt").open() as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
5 | contents = f.read()
|
||||
|
|
||||
help: Replace with `Path("file.txt").read_text()`
|
||||
1 |
|
||||
2 | from pathlib import Path
|
||||
3 |
|
||||
- with Path("file.txt").open() as f:
|
||||
- contents = f.read()
|
||||
4 + contents = Path("file.txt").read_text()
|
||||
5 |
|
||||
6 | with Path("file.txt").open("r") as f:
|
||||
7 | contents = f.read()
|
||||
|
||||
FURB101 [*] `Path.open()` followed by `read()` can be replaced by `Path("file.txt").read_text()`
|
||||
--> FURB101_1.py:7:6
|
||||
|
|
||||
5 | contents = f.read()
|
||||
6 |
|
||||
7 | with Path("file.txt").open("r") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
8 | contents = f.read()
|
||||
|
|
||||
help: Replace with `Path("file.txt").read_text()`
|
||||
4 | with Path("file.txt").open() as f:
|
||||
5 | contents = f.read()
|
||||
6 |
|
||||
- with Path("file.txt").open("r") as f:
|
||||
- contents = f.read()
|
||||
7 + contents = Path("file.txt").read_text()
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")`
|
||||
--> FURB103.py:12:6
|
||||
--> FURB103_0.py:12:6
|
||||
|
|
||||
11 | # FURB103
|
||||
12 | with open("file.txt", "w") as f:
|
||||
@@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").write_text("test")`
|
||||
16 | with open("file.txt", "wb") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)`
|
||||
--> FURB103.py:16:6
|
||||
--> FURB103_0.py:16:6
|
||||
|
|
||||
15 | # FURB103
|
||||
16 | with open("file.txt", "wb") as f:
|
||||
@@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").write_bytes(foobar)`
|
||||
20 | with open("file.txt", mode="wb") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")`
|
||||
--> FURB103.py:20:6
|
||||
--> FURB103_0.py:20:6
|
||||
|
|
||||
19 | # FURB103
|
||||
20 | with open("file.txt", mode="wb") as f:
|
||||
@@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").write_bytes(b"abc")`
|
||||
24 | with open("file.txt", "w", encoding="utf8") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")`
|
||||
--> FURB103.py:24:6
|
||||
--> FURB103_0.py:24:6
|
||||
|
|
||||
23 | # FURB103
|
||||
24 | with open("file.txt", "w", encoding="utf8") as f:
|
||||
@@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")`
|
||||
28 | with open("file.txt", "w", errors="ignore") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")`
|
||||
--> FURB103.py:28:6
|
||||
--> FURB103_0.py:28:6
|
||||
|
|
||||
27 | # FURB103
|
||||
28 | with open("file.txt", "w", errors="ignore") as f:
|
||||
@@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")`
|
||||
32 | with open("file.txt", mode="w") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)`
|
||||
--> FURB103.py:32:6
|
||||
--> FURB103_0.py:32:6
|
||||
|
|
||||
31 | # FURB103
|
||||
32 | with open("file.txt", mode="w") as f:
|
||||
@@ -146,7 +146,7 @@ help: Replace with `Path("file.txt").write_text(foobar)`
|
||||
36 | with open(foo(), "wb") as f:
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())`
|
||||
--> FURB103.py:36:6
|
||||
--> FURB103_0.py:36:6
|
||||
|
|
||||
35 | # FURB103
|
||||
36 | with open(foo(), "wb") as f:
|
||||
@@ -157,7 +157,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())
|
||||
help: Replace with `Path(foo()).write_bytes(bar())`
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
|
||||
--> FURB103.py:44:6
|
||||
--> FURB103_0.py:44:6
|
||||
|
|
||||
43 | # FURB103
|
||||
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
|
||||
@@ -168,7 +168,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
|
||||
help: Replace with `Path("a.txt").write_text(x)`
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
|
||||
--> FURB103.py:44:31
|
||||
--> FURB103_0.py:44:31
|
||||
|
|
||||
43 | # FURB103
|
||||
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
|
||||
@@ -179,7 +179,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
|
||||
help: Replace with `Path("b.txt").write_bytes(y)`
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(bar(bar(a + x)))`
|
||||
--> FURB103.py:49:18
|
||||
--> FURB103_0.py:49:18
|
||||
|
|
||||
48 | # FURB103
|
||||
49 | with foo() as a, open("file.txt", "w") as b, foo() as c:
|
||||
@@ -190,7 +190,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba
|
||||
help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
--> FURB103.py:58:6
|
||||
--> FURB103_0.py:58:6
|
||||
|
|
||||
57 | # FURB103
|
||||
58 | with open("file.txt", "w", newline="\r\n") as f:
|
||||
@@ -214,7 +214,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
62 | import builtins
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
--> FURB103.py:66:6
|
||||
--> FURB103_0.py:66:6
|
||||
|
|
||||
65 | # FURB103
|
||||
66 | with builtins.open("file.txt", "w", newline="\r\n") as f:
|
||||
@@ -237,7 +237,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
70 | from builtins import open as o
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
--> FURB103.py:74:6
|
||||
--> FURB103_0.py:74:6
|
||||
|
|
||||
73 | # FURB103
|
||||
74 | with o("file.txt", "w", newline="\r\n") as f:
|
||||
@@ -260,7 +260,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
|
||||
78 |
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
|
||||
--> FURB103.py:154:6
|
||||
--> FURB103_0.py:154:6
|
||||
|
|
||||
152 | data = {"price": 100}
|
||||
153 |
|
||||
@@ -284,7 +284,7 @@ help: Replace with `Path("test.json")....`
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
|
||||
--> FURB103.py:158:6
|
||||
--> FURB103_0.py:158:6
|
||||
|
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
|
||||
--> FURB103_1.py:3:6
|
||||
|
|
||||
1 | from pathlib import Path
|
||||
2 |
|
||||
3 | with Path("file.txt").open("w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
4 | f.write("test")
|
||||
|
|
||||
help: Replace with `Path("file.txt").write_text("test")`
|
||||
1 | from pathlib import Path
|
||||
2 |
|
||||
- with Path("file.txt").open("w") as f:
|
||||
- f.write("test")
|
||||
3 + Path("file.txt").write_text("test")
|
||||
4 |
|
||||
5 | with Path("file.txt").open("wb") as f:
|
||||
6 | f.write(b"test")
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_bytes(b"test")`
|
||||
--> FURB103_1.py:6:6
|
||||
|
|
||||
4 | f.write("test")
|
||||
5 |
|
||||
6 | with Path("file.txt").open("wb") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
7 | f.write(b"test")
|
||||
|
|
||||
help: Replace with `Path("file.txt").write_bytes(b"test")`
|
||||
3 | with Path("file.txt").open("w") as f:
|
||||
4 | f.write("test")
|
||||
5 |
|
||||
- with Path("file.txt").open("wb") as f:
|
||||
- f.write(b"test")
|
||||
6 + Path("file.txt").write_bytes(b"test")
|
||||
7 |
|
||||
8 | with Path("file.txt").open(mode="w") as f:
|
||||
9 | f.write("test")
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
|
||||
--> FURB103_1.py:9:6
|
||||
|
|
||||
7 | f.write(b"test")
|
||||
8 |
|
||||
9 | with Path("file.txt").open(mode="w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
10 | f.write("test")
|
||||
|
|
||||
help: Replace with `Path("file.txt").write_text("test")`
|
||||
6 | with Path("file.txt").open("wb") as f:
|
||||
7 | f.write(b"test")
|
||||
8 |
|
||||
- with Path("file.txt").open(mode="w") as f:
|
||||
- f.write("test")
|
||||
9 + Path("file.txt").write_text("test")
|
||||
10 |
|
||||
11 | with Path("file.txt").open("w", encoding="utf8") as f:
|
||||
12 | f.write("test")
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", encoding="utf8")`
|
||||
--> FURB103_1.py:12:6
|
||||
|
|
||||
10 | f.write("test")
|
||||
11 |
|
||||
12 | with Path("file.txt").open("w", encoding="utf8") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
13 | f.write("test")
|
||||
|
|
||||
help: Replace with `Path("file.txt").write_text("test", encoding="utf8")`
|
||||
9 | with Path("file.txt").open(mode="w") as f:
|
||||
10 | f.write("test")
|
||||
11 |
|
||||
- with Path("file.txt").open("w", encoding="utf8") as f:
|
||||
- f.write("test")
|
||||
12 + Path("file.txt").write_text("test", encoding="utf8")
|
||||
13 |
|
||||
14 | with Path("file.txt").open("w", errors="ignore") as f:
|
||||
15 | f.write("test")
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", errors="ignore")`
|
||||
--> FURB103_1.py:15:6
|
||||
|
|
||||
13 | f.write("test")
|
||||
14 |
|
||||
15 | with Path("file.txt").open("w", errors="ignore") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
16 | f.write("test")
|
||||
|
|
||||
help: Replace with `Path("file.txt").write_text("test", errors="ignore")`
|
||||
12 | with Path("file.txt").open("w", encoding="utf8") as f:
|
||||
13 | f.write("test")
|
||||
14 |
|
||||
- with Path("file.txt").open("w", errors="ignore") as f:
|
||||
- f.write("test")
|
||||
15 + Path("file.txt").write_text("test", errors="ignore")
|
||||
16 |
|
||||
17 | with Path(foo()).open("w") as f:
|
||||
18 | f.write("test")
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path(foo()).write_text("test")`
|
||||
--> FURB103_1.py:18:6
|
||||
|
|
||||
16 | f.write("test")
|
||||
17 |
|
||||
18 | with Path(foo()).open("w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
19 | f.write("test")
|
||||
|
|
||||
help: Replace with `Path(foo()).write_text("test")`
|
||||
15 | with Path("file.txt").open("w", errors="ignore") as f:
|
||||
16 | f.write("test")
|
||||
17 |
|
||||
- with Path(foo()).open("w") as f:
|
||||
- f.write("test")
|
||||
18 + Path(foo()).write_text("test")
|
||||
19 |
|
||||
20 | p = Path("file.txt")
|
||||
21 | with p.open("w") as f:
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `p.write_text("test")`
|
||||
--> FURB103_1.py:22:6
|
||||
|
|
||||
21 | p = Path("file.txt")
|
||||
22 | with p.open("w") as f:
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
23 | f.write("test")
|
||||
|
|
||||
help: Replace with `p.write_text("test")`
|
||||
19 | f.write("test")
|
||||
20 |
|
||||
21 | p = Path("file.txt")
|
||||
- with p.open("w") as f:
|
||||
- f.write("test")
|
||||
22 + p.write_text("test")
|
||||
23 |
|
||||
24 | with Path("foo", "bar", "baz").open("w") as f:
|
||||
25 | f.write("test")
|
||||
|
||||
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("foo", "bar", "baz").write_text("test")`
|
||||
--> FURB103_1.py:25:6
|
||||
|
|
||||
23 | f.write("test")
|
||||
24 |
|
||||
25 | with Path("foo", "bar", "baz").open("w") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
26 | f.write("test")
|
||||
|
|
||||
help: Replace with `Path("foo", "bar", "baz").write_text("test")`
|
||||
22 | with p.open("w") as f:
|
||||
23 | f.write("test")
|
||||
24 |
|
||||
- with Path("foo", "bar", "baz").open("w") as f:
|
||||
- f.write("test")
|
||||
25 + Path("foo", "bar", "baz").write_text("test")
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")`
|
||||
--> FURB103.py:12:6
|
||||
--> FURB103_0.py:12:6
|
||||
|
|
||||
11 | # FURB103
|
||||
12 | with open("file.txt", "w") as f:
|
||||
@@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").write_text("test")`
|
||||
16 | with open("file.txt", "wb") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)`
|
||||
--> FURB103.py:16:6
|
||||
--> FURB103_0.py:16:6
|
||||
|
|
||||
15 | # FURB103
|
||||
16 | with open("file.txt", "wb") as f:
|
||||
@@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").write_bytes(foobar)`
|
||||
20 | with open("file.txt", mode="wb") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")`
|
||||
--> FURB103.py:20:6
|
||||
--> FURB103_0.py:20:6
|
||||
|
|
||||
19 | # FURB103
|
||||
20 | with open("file.txt", mode="wb") as f:
|
||||
@@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").write_bytes(b"abc")`
|
||||
24 | with open("file.txt", "w", encoding="utf8") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")`
|
||||
--> FURB103.py:24:6
|
||||
--> FURB103_0.py:24:6
|
||||
|
|
||||
23 | # FURB103
|
||||
24 | with open("file.txt", "w", encoding="utf8") as f:
|
||||
@@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")`
|
||||
28 | with open("file.txt", "w", errors="ignore") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")`
|
||||
--> FURB103.py:28:6
|
||||
--> FURB103_0.py:28:6
|
||||
|
|
||||
27 | # FURB103
|
||||
28 | with open("file.txt", "w", errors="ignore") as f:
|
||||
@@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")`
|
||||
32 | with open("file.txt", mode="w") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)`
|
||||
--> FURB103.py:32:6
|
||||
--> FURB103_0.py:32:6
|
||||
|
|
||||
31 | # FURB103
|
||||
32 | with open("file.txt", mode="w") as f:
|
||||
@@ -146,7 +146,7 @@ help: Replace with `Path("file.txt").write_text(foobar)`
|
||||
36 | with open(foo(), "wb") as f:
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())`
|
||||
--> FURB103.py:36:6
|
||||
--> FURB103_0.py:36:6
|
||||
|
|
||||
35 | # FURB103
|
||||
36 | with open(foo(), "wb") as f:
|
||||
@@ -157,7 +157,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())
|
||||
help: Replace with `Path(foo()).write_bytes(bar())`
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
|
||||
--> FURB103.py:44:6
|
||||
--> FURB103_0.py:44:6
|
||||
|
|
||||
43 | # FURB103
|
||||
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
|
||||
@@ -168,7 +168,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
|
||||
help: Replace with `Path("a.txt").write_text(x)`
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
|
||||
--> FURB103.py:44:31
|
||||
--> FURB103_0.py:44:31
|
||||
|
|
||||
43 | # FURB103
|
||||
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
|
||||
@@ -179,7 +179,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
|
||||
help: Replace with `Path("b.txt").write_bytes(y)`
|
||||
|
||||
FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(bar(bar(a + x)))`
|
||||
--> FURB103.py:49:18
|
||||
--> FURB103_0.py:49:18
|
||||
|
|
||||
48 | # FURB103
|
||||
49 | with foo() as a, open("file.txt", "w") as b, foo() as c:
|
||||
@@ -190,7 +190,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba
|
||||
help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
|
||||
--> FURB103.py:154:6
|
||||
--> FURB103_0.py:154:6
|
||||
|
|
||||
152 | data = {"price": 100}
|
||||
153 |
|
||||
@@ -214,7 +214,7 @@ help: Replace with `Path("test.json")....`
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
|
||||
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
|
||||
--> FURB103.py:158:6
|
||||
--> FURB103_0.py:158:6
|
||||
|
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: annotated name `a` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:4:5
|
||||
|
|
||||
2 | def f1():
|
||||
3 | global a
|
||||
4 | a: str = "foo" # error
|
||||
| ^
|
||||
5 |
|
||||
6 | b: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `b` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:10:9
|
||||
|
|
||||
8 | def inner():
|
||||
9 | global b
|
||||
10 | b: str = "nested" # error
|
||||
| ^
|
||||
11 |
|
||||
12 | c: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `c` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:15:5
|
||||
|
|
||||
13 | def f2():
|
||||
14 | global c
|
||||
15 | c: list[str] = [] # error
|
||||
| ^
|
||||
16 |
|
||||
17 | d: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `d` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:20:5
|
||||
|
|
||||
18 | def f3():
|
||||
19 | global d
|
||||
20 | d: str # error
|
||||
| ^
|
||||
21 |
|
||||
22 | e: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `g` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:29:1
|
||||
|
|
||||
27 | f: int = 1 # okay
|
||||
28 |
|
||||
29 | g: int = 1
|
||||
| ^
|
||||
30 | global g # error
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `x` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:33:5
|
||||
|
|
||||
32 | class C:
|
||||
33 | x: str
|
||||
| ^
|
||||
34 | global x # error
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `x` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:38:5
|
||||
|
|
||||
36 | class D:
|
||||
37 | global x # error
|
||||
38 | x: str
|
||||
| ^
|
||||
|
|
||||
@@ -592,11 +592,23 @@ impl FormatString {
|
||||
fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
|
||||
let mut cur_text = text;
|
||||
let mut result_string = String::new();
|
||||
let mut pending_escape = false;
|
||||
while !cur_text.is_empty() {
|
||||
if pending_escape
|
||||
&& let Some((unicode_string, remaining)) =
|
||||
FormatString::parse_escaped_unicode_string(cur_text)
|
||||
{
|
||||
result_string.push_str(unicode_string);
|
||||
cur_text = remaining;
|
||||
pending_escape = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
match FormatString::parse_literal_single(cur_text) {
|
||||
Ok((next_char, remaining)) => {
|
||||
result_string.push(next_char);
|
||||
cur_text = remaining;
|
||||
pending_escape = next_char == '\\' && !pending_escape;
|
||||
}
|
||||
Err(err) => {
|
||||
return if result_string.is_empty() {
|
||||
@@ -678,6 +690,13 @@ impl FormatString {
|
||||
}
|
||||
Err(FormatParseError::UnmatchedBracket)
|
||||
}
|
||||
|
||||
fn parse_escaped_unicode_string(text: &str) -> Option<(&str, &str)> {
|
||||
text.strip_prefix("N{")?.find('}').map(|idx| {
|
||||
let end_idx = idx + 3; // 3 for "N{"
|
||||
(&text[..end_idx], &text[end_idx..])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FromTemplate<'a>: Sized {
|
||||
@@ -1020,4 +1039,48 @@ mod tests {
|
||||
Err(FormatParseError::InvalidCharacterAfterRightBracket)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_unicode_escape() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![FormatPart::Literal("I am a \\N{snowman}".to_owned())],
|
||||
});
|
||||
|
||||
assert_eq!(FormatString::from_str("I am a \\N{snowman}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_unicode_escape_with_field() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![
|
||||
FormatPart::Literal("I am a \\N{snowman}".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "snowman".to_owned(),
|
||||
conversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
FormatString::from_str("I am a \\N{snowman}{snowman}"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_multiple_escape_with_field() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![
|
||||
FormatPart::Literal("I am a \\\\N".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "snowman".to_owned(),
|
||||
conversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(FormatString::from_str("I am a \\\\N{snowman}"), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,9 @@ impl SemanticSyntaxChecker {
|
||||
|
||||
fn check_annotation<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
|
||||
match stmt {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target, annotation, ..
|
||||
}) => {
|
||||
if ctx.python_version() > PythonVersion::PY313 {
|
||||
// test_ok valid_annotation_py313
|
||||
// # parse_options: {"target-version": "3.13"}
|
||||
@@ -297,6 +299,18 @@ impl SemanticSyntaxChecker {
|
||||
};
|
||||
visitor.visit_expr(annotation);
|
||||
}
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
if let Some(global_stmt) = ctx.global(id.as_str()) {
|
||||
let global_start = global_stmt.start();
|
||||
if !ctx.in_module_scope() || target.start() < global_start {
|
||||
Self::add_error(
|
||||
ctx,
|
||||
SemanticSyntaxErrorKind::AnnotatedGlobal(id.to_string()),
|
||||
target.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
type_params,
|
||||
|
||||
16
crates/ty/docs/configuration.md
generated
16
crates/ty/docs/configuration.md
generated
@@ -200,24 +200,22 @@ Configuration override that applies to specific files based on glob patterns.
|
||||
|
||||
An override allows you to apply different rule configurations to specific
|
||||
files or directories. Multiple overrides can match the same file, with
|
||||
later overrides take precedence.
|
||||
later overrides take precedence. Override rules take precedence over global
|
||||
rules for matching files.
|
||||
|
||||
### Precedence
|
||||
|
||||
- Later overrides in the array take precedence over earlier ones
|
||||
- Override rules take precedence over global rules for matching files
|
||||
|
||||
### Examples
|
||||
For example, to relax enforcement of rules in test files:
|
||||
|
||||
```toml
|
||||
# Relax rules for test files
|
||||
[[tool.ty.overrides]]
|
||||
include = ["tests/**", "**/test_*.py"]
|
||||
|
||||
[tool.ty.overrides.rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
```
|
||||
|
||||
# Ignore generated files but still check important ones
|
||||
Or, to ignore a rule in generated files but retain enforcement in an important file:
|
||||
|
||||
```toml
|
||||
[[tool.ty.overrides]]
|
||||
include = ["generated/**"]
|
||||
exclude = ["generated/important.py"]
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
ty defines and respects the following environment variables:
|
||||
|
||||
### `TY_CONFIG_FILE`
|
||||
|
||||
Path to a `ty.toml` configuration file to use.
|
||||
|
||||
When set, ty will use this file for configuration instead of
|
||||
discovering configuration files automatically.
|
||||
|
||||
Equivalent to the `--config-file` command-line argument.
|
||||
|
||||
### `TY_LOG`
|
||||
|
||||
If set, ty will use this value as the log level for its `--verbose` output.
|
||||
|
||||
@@ -9,6 +9,7 @@ use ty_combine::Combine;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions};
|
||||
use ty_project::metadata::value::{RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource};
|
||||
use ty_python_semantic::lint;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
// Configures Clap v3-style help menu colors
|
||||
const STYLES: Styles = Styles::styled()
|
||||
@@ -121,7 +122,7 @@ pub(crate) struct CheckCommand {
|
||||
/// The path to a `ty.toml` file to use for configuration.
|
||||
///
|
||||
/// While ty configuration can be included in a `pyproject.toml` file, it is not allowed in this context.
|
||||
#[arg(long, env = "TY_CONFIG_FILE", value_name = "PATH")]
|
||||
#[arg(long, env = EnvVars::TY_CONFIG_FILE, value_name = "PATH")]
|
||||
pub(crate) config_file: Option<SystemPathBuf>,
|
||||
|
||||
/// The format to use for printing diagnostic messages.
|
||||
|
||||
@@ -515,6 +515,18 @@ fn detect_function_arg_completions<'db>(
|
||||
parsed: &ParsedModuleRef,
|
||||
offset: TextSize,
|
||||
) -> Option<Vec<Completion<'db>>> {
|
||||
// But be careful: this isn't as simple as just finding a call
|
||||
// expression. We also have to make sure we are in the "arguments"
|
||||
// portion of the call. Otherwise we risk incorrectly returning
|
||||
// something for `(<CURSOR>)(arg1, arg2)`-style expressions.
|
||||
if !covering_node(parsed.syntax().into(), TextRange::empty(offset))
|
||||
.ancestors()
|
||||
.take_while(|node| !node.is_statement())
|
||||
.any(|node| node.is_arguments())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let sig_help = signature_help(db, file, offset)?;
|
||||
let set_function_args = detect_set_function_args(parsed, offset);
|
||||
|
||||
@@ -2594,9 +2606,7 @@ def frob(): ...
|
||||
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().build().snapshot(),
|
||||
@r"
|
||||
foo
|
||||
",
|
||||
@"<No completions found after filtering out completions>",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2610,9 +2620,7 @@ def frob(): ...
|
||||
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().build().snapshot(),
|
||||
@r"
|
||||
foo
|
||||
",
|
||||
@"<No completions found after filtering out completions>",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3204,7 +3212,7 @@ quux.<CURSOR>
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
|
||||
count :: bound method Quux.count(value: Any, /) -> int
|
||||
index :: bound method Quux.index(value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
|
||||
index :: bound method Quux.index(value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int
|
||||
x :: int
|
||||
y :: str
|
||||
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]]
|
||||
@@ -3269,7 +3277,7 @@ bar(o<CURSOR>
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
foo
|
||||
okay
|
||||
okay=
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3290,7 +3298,7 @@ bar(o<CURSOR>
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
foo
|
||||
okay
|
||||
okay=
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3308,9 +3316,9 @@ foo(b<CURSOR>
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
bar
|
||||
barbaz
|
||||
baz
|
||||
bar=
|
||||
barbaz=
|
||||
baz=
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3327,9 +3335,7 @@ foo(bar=1, b<CURSOR>
|
||||
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
baz
|
||||
"
|
||||
@"baz="
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3347,9 +3353,7 @@ abc(o<CURSOR>
|
||||
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
okay
|
||||
"
|
||||
@"okay="
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3365,9 +3369,7 @@ abc(okay=1, ba<CURSOR> baz=5
|
||||
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
bar
|
||||
"
|
||||
@"bar="
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3411,9 +3413,9 @@ bar(o<CURSOR>
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@r"
|
||||
foo
|
||||
okay
|
||||
okay_abc
|
||||
okay_okay
|
||||
okay=
|
||||
okay_abc=
|
||||
okay_okay=
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3433,7 +3435,7 @@ bar(<CURSOR>
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
||||
bar
|
||||
foo
|
||||
okay
|
||||
okay=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -6715,6 +6717,27 @@ def f(zqzqzq: str):
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_import_prioritizes_reusing_import_from_statements() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
import typing
|
||||
from typing import Callable
|
||||
TypedDi<CURSOR>
|
||||
",
|
||||
);
|
||||
assert_snapshot!(
|
||||
builder.imports().build().snapshot(),
|
||||
@r"
|
||||
TypedDict :: , TypedDict
|
||||
is_typeddict :: , is_typeddict
|
||||
_FilterConfigurationTypedDict :: from logging.config import _FilterConfigurationTypedDict
|
||||
|
||||
_FormatterConfigurationTypedDict :: from logging.config import _FormatterConfigurationTypedDict
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) completion test
|
||||
/// builder.
|
||||
///
|
||||
@@ -6740,6 +6763,7 @@ def f(zqzqzq: str):
|
||||
skip_builtins: bool,
|
||||
skip_keywords: bool,
|
||||
type_signatures: bool,
|
||||
imports: bool,
|
||||
module_names: bool,
|
||||
// This doesn't seem like a "very complex" type to me... ---AG
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -6772,6 +6796,7 @@ def f(zqzqzq: str):
|
||||
original,
|
||||
filtered,
|
||||
type_signatures: self.type_signatures,
|
||||
imports: self.imports,
|
||||
module_names: self.module_names,
|
||||
}
|
||||
}
|
||||
@@ -6832,6 +6857,15 @@ def f(zqzqzq: str):
|
||||
self
|
||||
}
|
||||
|
||||
/// When set, include the import associated with the
|
||||
/// completion.
|
||||
///
|
||||
/// Not enabled by default.
|
||||
fn imports(mut self) -> CompletionTestBuilder {
|
||||
self.imports = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// When set, the module name for each symbol is included
|
||||
/// in the snapshot (if available).
|
||||
fn module_names(mut self) -> CompletionTestBuilder {
|
||||
@@ -6864,6 +6898,9 @@ def f(zqzqzq: str):
|
||||
/// Whether type signatures should be included in the snapshot
|
||||
/// generated by `CompletionTest::snapshot`.
|
||||
type_signatures: bool,
|
||||
/// Whether to show the import that will be inserted when this
|
||||
/// completion is selected.
|
||||
imports: bool,
|
||||
/// Whether module names should be included in the snapshot
|
||||
/// generated by `CompletionTest::snapshot`.
|
||||
module_names: bool,
|
||||
@@ -6885,7 +6922,7 @@ def f(zqzqzq: str):
|
||||
self.filtered
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut snapshot = c.name.as_str().to_string();
|
||||
let mut snapshot = c.insert.as_deref().unwrap_or(c.name.as_str()).to_string();
|
||||
if self.type_signatures {
|
||||
let ty =
|
||||
c.ty.map(|ty| ty.display(self.db).to_string())
|
||||
@@ -6899,6 +6936,17 @@ def f(zqzqzq: str):
|
||||
.unwrap_or("<no import required>");
|
||||
snapshot = format!("{snapshot} :: {module_name}");
|
||||
}
|
||||
if self.imports {
|
||||
if let Some(ref edit) = c.import {
|
||||
if let Some(import) = edit.content() {
|
||||
snapshot = format!("{snapshot} :: {import}");
|
||||
} else {
|
||||
snapshot = format!("{snapshot} :: <import deletion>");
|
||||
}
|
||||
} else {
|
||||
snapshot = format!("{snapshot} :: <no import edit>");
|
||||
}
|
||||
}
|
||||
snapshot
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
@@ -6947,6 +6995,7 @@ def f(zqzqzq: str):
|
||||
skip_builtins: false,
|
||||
skip_keywords: false,
|
||||
type_signatures: false,
|
||||
imports: false,
|
||||
module_names: false,
|
||||
predicate: None,
|
||||
}
|
||||
|
||||
@@ -151,14 +151,19 @@ impl fmt::Display for DisplayHoverContent<'_, '_> {
|
||||
Some(TypeVarVariance::Bivariant) => " (bivariant)",
|
||||
None => "",
|
||||
};
|
||||
|
||||
// Special types like `<special-form of whatever 'blahblah' with 'florps'>`
|
||||
// render poorly with python syntax-highlighting but well as xml
|
||||
let ty_string = ty
|
||||
.display_with(self.db, DisplaySettings::default().multiline())
|
||||
.to_string();
|
||||
let syntax = if ty_string.starts_with('<') {
|
||||
"xml"
|
||||
} else {
|
||||
"python"
|
||||
};
|
||||
self.kind
|
||||
.fenced_code_block(
|
||||
format!(
|
||||
"{}{variance}",
|
||||
ty.display_with(self.db, DisplaySettings::default().multiline())
|
||||
),
|
||||
"python",
|
||||
)
|
||||
.fenced_code_block(format!("{ty_string}{variance}"), syntax)
|
||||
.fmt(f)
|
||||
}
|
||||
HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f),
|
||||
@@ -358,7 +363,7 @@ mod tests {
|
||||
Everyone loves my class!!
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
@@ -420,7 +425,7 @@ mod tests {
|
||||
Everyone loves my class!!
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
@@ -480,7 +485,7 @@ mod tests {
|
||||
initializes MyClass (perfectly)
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
@@ -536,7 +541,7 @@ mod tests {
|
||||
initializes MyClass (perfectly)
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
@@ -595,7 +600,7 @@ mod tests {
|
||||
Everyone loves my class!!
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
@@ -1178,13 +1183,13 @@ def ab(a: str): ...
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
---------------------------------------------
|
||||
the int overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
```
|
||||
---
|
||||
the int overload
|
||||
@@ -1238,13 +1243,13 @@ def ab(a: str):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(a: str) -> Unknown
|
||||
def ab(a: str) -> Unknown
|
||||
---------------------------------------------
|
||||
the int overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: str) -> Unknown
|
||||
def ab(a: str) -> Unknown
|
||||
```
|
||||
---
|
||||
the int overload
|
||||
@@ -1298,7 +1303,7 @@ def ab(a: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
b: int
|
||||
) -> Unknown
|
||||
@@ -1307,7 +1312,7 @@ def ab(a: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
b: int
|
||||
) -> Unknown
|
||||
@@ -1364,13 +1369,13 @@ def ab(a: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
---------------------------------------------
|
||||
the two arg overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
```
|
||||
---
|
||||
the two arg overload
|
||||
@@ -1428,7 +1433,7 @@ def ab(a: int, *, c: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
b: int
|
||||
@@ -1438,7 +1443,7 @@ def ab(a: int, *, c: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
b: int
|
||||
@@ -1500,7 +1505,7 @@ def ab(a: int, *, c: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
c: int
|
||||
@@ -1510,7 +1515,7 @@ def ab(a: int, *, c: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
c: int
|
||||
@@ -1559,11 +1564,11 @@ def ab(a: int, *, c: int):
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(
|
||||
def foo(
|
||||
a: int,
|
||||
b
|
||||
) -> Unknown
|
||||
(
|
||||
def foo(
|
||||
a: str,
|
||||
b
|
||||
) -> Unknown
|
||||
@@ -1572,11 +1577,11 @@ def ab(a: int, *, c: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def foo(
|
||||
a: int,
|
||||
b
|
||||
) -> Unknown
|
||||
(
|
||||
def foo(
|
||||
a: str,
|
||||
b
|
||||
) -> Unknown
|
||||
@@ -1623,15 +1628,15 @@ def ab(a: int, *, c: int):
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(a: int) -> Unknown
|
||||
(a: str) -> Unknown
|
||||
def foo(a: int) -> Unknown
|
||||
def foo(a: str) -> Unknown
|
||||
---------------------------------------------
|
||||
The first overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
(a: str) -> Unknown
|
||||
def foo(a: int) -> Unknown
|
||||
def foo(a: str) -> Unknown
|
||||
```
|
||||
---
|
||||
The first overload
|
||||
@@ -1680,7 +1685,7 @@ def ab(a: int, *, c: int):
|
||||
Wow this module rocks.
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<module 'lib'>
|
||||
```
|
||||
---
|
||||
@@ -2029,7 +2034,7 @@ def function():
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<class 'Click'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<class 'Click'>
|
||||
```
|
||||
---------------------------------------------
|
||||
@@ -2234,7 +2239,7 @@ def function():
|
||||
Wow this module rocks.
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<module 'lib'>
|
||||
```
|
||||
---
|
||||
@@ -2734,6 +2739,86 @@ def function():
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_dunder_doc() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class My<CURSOR>Class:
|
||||
__doc__ = "hello there"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
<class 'MyClass'>
|
||||
---------------------------------------------
|
||||
hello there
|
||||
|
||||
---------------------------------------------
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---
|
||||
hello there
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^-^^^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 | __doc__ = "hello there"
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_dunder_doc_complex() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class My<CURSOR>Class:
|
||||
__doc__ = (
|
||||
r"""This is some extremely complex docstring
|
||||
|
||||
Designed to make
|
||||
"""
|
||||
+ r"""
|
||||
|
||||
A typechecker
|
||||
"""
|
||||
"""
|
||||
Fall to its knees and sob
|
||||
"""
|
||||
r"""
|
||||
Witness my works and
|
||||
weep before them
|
||||
"""
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
<class 'MyClass'>
|
||||
---------------------------------------------
|
||||
```xml
|
||||
<class 'MyClass'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^-^^^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 | __doc__ = (
|
||||
4 | r"""This is some extremely complex docstring
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_class_typevar_variance() {
|
||||
let test = cursor_test(
|
||||
@@ -3228,12 +3313,12 @@ def function():
|
||||
// TODO: We should only show the matching overload here.
|
||||
// https://github.com/astral-sh/ty/issues/73
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(other: Test, /) -> Test
|
||||
(other: Other, /) -> Test
|
||||
def __add__(other: Test, /) -> Test
|
||||
def __add__(other: Other, /) -> Test
|
||||
---------------------------------------------
|
||||
```python
|
||||
(other: Test, /) -> Test
|
||||
(other: Other, /) -> Test
|
||||
def __add__(other: Test, /) -> Test
|
||||
def __add__(other: Other, /) -> Test
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
@@ -3343,7 +3428,7 @@ def function():
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<module 'mypackage.subpkg'>
|
||||
```
|
||||
---------------------------------------------
|
||||
@@ -3385,7 +3470,7 @@ def function():
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<module 'mypackage.subpkg'>
|
||||
```
|
||||
---------------------------------------------
|
||||
@@ -3469,7 +3554,7 @@ def function():
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg.submod'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<module 'mypackage.subpkg.submod'>
|
||||
```
|
||||
---------------------------------------------
|
||||
@@ -3510,7 +3595,7 @@ def function():
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
```xml
|
||||
<module 'mypackage.subpkg'>
|
||||
```
|
||||
---------------------------------------------
|
||||
|
||||
@@ -745,8 +745,17 @@ impl ImportResponseKind<'_> {
|
||||
fn priority(&self) -> usize {
|
||||
match *self {
|
||||
ImportResponseKind::Unqualified { .. } => 0,
|
||||
ImportResponseKind::Qualified { .. } => 1,
|
||||
ImportResponseKind::Partial(_) => 2,
|
||||
ImportResponseKind::Partial(_) => 1,
|
||||
// N.B. When given the choice between adding a
|
||||
// name to an existing `from ... import ...`
|
||||
// statement and using an existing `import ...`
|
||||
// in a qualified manner, we currently choose
|
||||
// the former. Originally we preferred qualification,
|
||||
// but there is some evidence that this violates
|
||||
// expectations.
|
||||
//
|
||||
// Ref: https://github.com/astral-sh/ty/issues/1274#issuecomment-3352233790
|
||||
ImportResponseKind::Qualified { .. } => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1332,9 +1341,9 @@ import collections
|
||||
);
|
||||
assert_snapshot!(
|
||||
test.import("collections", "defaultdict"), @r"
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, defaultdict
|
||||
import collections
|
||||
collections.defaultdict
|
||||
defaultdict
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -6216,7 +6216,7 @@ mod tests {
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
from typing import Literal
|
||||
|
||||
a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
a[: <special-form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
@@ -6232,7 +6232,7 @@ mod tests {
|
||||
|
|
||||
2 | from typing import Literal
|
||||
3 |
|
||||
4 | a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
4 | a[: <special-form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
| ^^^^^^^
|
||||
|
|
||||
|
||||
@@ -6250,7 +6250,7 @@ mod tests {
|
||||
|
|
||||
2 | from typing import Literal
|
||||
3 |
|
||||
4 | a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
4 | a[: <special-form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
| ^^^
|
||||
|
|
||||
|
||||
@@ -6268,7 +6268,7 @@ mod tests {
|
||||
|
|
||||
2 | from typing import Literal
|
||||
3 |
|
||||
4 | a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
4 | a[: <special-form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
| ^^^
|
||||
|
|
||||
|
||||
@@ -6286,7 +6286,7 @@ mod tests {
|
||||
|
|
||||
2 | from typing import Literal
|
||||
3 |
|
||||
4 | a[: <special form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
4 | a[: <special-form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
| ^^^
|
||||
|
|
||||
"#);
|
||||
@@ -6636,7 +6636,7 @@ mod tests {
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
from typing import Protocol, TypeVar
|
||||
T = TypeVar([name=]'T')
|
||||
Strange[: <special form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
Strange[: <special-form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:276:13
|
||||
@@ -6654,7 +6654,7 @@ mod tests {
|
||||
2 | from typing import Protocol, TypeVar
|
||||
3 | T = TypeVar([name=]'T')
|
||||
| ^^^^
|
||||
4 | Strange[: <special form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
4 | Strange[: <special-form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
@@ -6671,7 +6671,7 @@ mod tests {
|
||||
|
|
||||
2 | from typing import Protocol, TypeVar
|
||||
3 | T = TypeVar([name=]'T')
|
||||
4 | Strange[: <special form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
4 | Strange[: <special-form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
@@ -6688,7 +6688,7 @@ mod tests {
|
||||
|
|
||||
2 | from typing import Protocol, TypeVar
|
||||
3 | T = TypeVar([name=]'T')
|
||||
4 | Strange[: <special form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
4 | Strange[: <special-form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
| ^
|
||||
|
|
||||
");
|
||||
|
||||
@@ -254,7 +254,9 @@ impl<'db> SemanticTokenVisitor<'db> {
|
||||
}
|
||||
|
||||
fn is_constant_name(name: &str) -> bool {
|
||||
name.chars().all(|c| c.is_uppercase() || c == '_') && name.len() > 1
|
||||
name.chars()
|
||||
.all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
|
||||
&& name.len() > 1
|
||||
}
|
||||
|
||||
fn classify_name(&self, name: &ast::ExprName) -> (SemanticTokenType, SemanticTokenModifier) {
|
||||
@@ -2230,6 +2232,49 @@ class MyClass:
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constant_variations() {
|
||||
let test = SemanticTokenTest::new(
|
||||
r#"
|
||||
A = 1
|
||||
AB = 1
|
||||
ABC = 1
|
||||
A1 = 1
|
||||
AB1 = 1
|
||||
ABC1 = 1
|
||||
A_B = 1
|
||||
A1_B = 1
|
||||
A_B1 = 1
|
||||
A_1 = 1
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = test.highlight_file();
|
||||
|
||||
assert_snapshot!(test.to_snapshot(&tokens), @r#"
|
||||
"A" @ 1..2: Variable [definition]
|
||||
"1" @ 5..6: Number
|
||||
"AB" @ 7..9: Variable [definition, readonly]
|
||||
"1" @ 12..13: Number
|
||||
"ABC" @ 14..17: Variable [definition, readonly]
|
||||
"1" @ 20..21: Number
|
||||
"A1" @ 22..24: Variable [definition, readonly]
|
||||
"1" @ 27..28: Number
|
||||
"AB1" @ 29..32: Variable [definition, readonly]
|
||||
"1" @ 35..36: Number
|
||||
"ABC1" @ 37..41: Variable [definition, readonly]
|
||||
"1" @ 44..45: Number
|
||||
"A_B" @ 46..49: Variable [definition, readonly]
|
||||
"1" @ 52..53: Number
|
||||
"A1_B" @ 54..58: Variable [definition, readonly]
|
||||
"1" @ 61..62: Number
|
||||
"A_B1" @ 63..67: Variable [definition, readonly]
|
||||
"1" @ 70..71: Number
|
||||
"A_1" @ 72..75: Variable [definition, readonly]
|
||||
"1" @ 78..79: Number
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_implicitly_concatenated_strings() {
|
||||
let test = SemanticTokenTest::new(
|
||||
|
||||
@@ -124,6 +124,11 @@ fn get_call_expr(
|
||||
})?;
|
||||
|
||||
// Find the covering node at the given position that is a function call.
|
||||
// Note that we are okay with the range being anywhere within a call
|
||||
// expression, even if it's not in the arguments portion of the call
|
||||
// expression. This is because, e.g., a user can request signature
|
||||
// information at a call site, and this should ideally work anywhere
|
||||
// within the call site, even at the function name.
|
||||
let call = covering_node(root_node, token.range())
|
||||
.find_first(|node| {
|
||||
if !node.is_expr_call() {
|
||||
|
||||
@@ -1225,24 +1225,22 @@ pub struct TerminalOptions {
|
||||
///
|
||||
/// An override allows you to apply different rule configurations to specific
|
||||
/// files or directories. Multiple overrides can match the same file, with
|
||||
/// later overrides take precedence.
|
||||
/// later overrides take precedence. Override rules take precedence over global
|
||||
/// rules for matching files.
|
||||
///
|
||||
/// ### Precedence
|
||||
///
|
||||
/// - Later overrides in the array take precedence over earlier ones
|
||||
/// - Override rules take precedence over global rules for matching files
|
||||
///
|
||||
/// ### Examples
|
||||
/// For example, to relax enforcement of rules in test files:
|
||||
///
|
||||
/// ```toml
|
||||
/// # Relax rules for test files
|
||||
/// [[tool.ty.overrides]]
|
||||
/// include = ["tests/**", "**/test_*.py"]
|
||||
///
|
||||
/// [tool.ty.overrides.rules]
|
||||
/// possibly-unresolved-reference = "warn"
|
||||
/// ```
|
||||
///
|
||||
/// # Ignore generated files but still check important ones
|
||||
/// Or, to ignore a rule in generated files but retain enforcement in an important file:
|
||||
///
|
||||
/// ```toml
|
||||
/// [[tool.ty.overrides]]
|
||||
/// include = ["generated/**"]
|
||||
/// exclude = ["generated/important.py"]
|
||||
|
||||
@@ -169,13 +169,13 @@ def f(x: Any[int]):
|
||||
`Any` cannot be called (this leads to a `TypeError` at runtime):
|
||||
|
||||
```py
|
||||
Any() # error: [call-non-callable] "Object of type `<special form 'typing.Any'>` is not callable"
|
||||
Any() # error: [call-non-callable] "Object of type `<special-form 'typing.Any'>` is not callable"
|
||||
```
|
||||
|
||||
`Any` also cannot be used as a metaclass (under the hood, this leads to an implicit call to `Any`):
|
||||
|
||||
```py
|
||||
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `<special form 'typing.Any'>` is not callable"
|
||||
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `<special-form 'typing.Any'>` is not callable"
|
||||
```
|
||||
|
||||
And `Any` cannot be used in `isinstance()` checks:
|
||||
|
||||
@@ -407,4 +407,22 @@ def f_okay(c: Callable[[], None]):
|
||||
c.__qualname__ = "my_callable" # okay
|
||||
```
|
||||
|
||||
## From a class
|
||||
|
||||
### Subclasses should return themselves, not superclass
|
||||
|
||||
```py
|
||||
from ty_extensions import into_callable
|
||||
|
||||
class Base:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
class A(Base):
|
||||
pass
|
||||
|
||||
# revealed: () -> A
|
||||
reveal_type(into_callable(A))
|
||||
```
|
||||
|
||||
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
|
||||
|
||||
@@ -205,3 +205,93 @@ class B:
|
||||
class A(B): ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
## Default argument values
|
||||
|
||||
### Not deferred in regular files
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
def f(mode: int = ParseMode.test):
|
||||
pass
|
||||
|
||||
class ParseMode:
|
||||
test = 1
|
||||
```
|
||||
|
||||
### Deferred in stub files
|
||||
|
||||
Forward references in default argument values are allowed in stub files.
|
||||
|
||||
```pyi
|
||||
def f(mode: int = ParseMode.test): ...
|
||||
|
||||
class ParseMode:
|
||||
test: int
|
||||
```
|
||||
|
||||
### Undefined names are still errors in stub files
|
||||
|
||||
```pyi
|
||||
# error: [unresolved-reference]
|
||||
def f(mode: int = NeverDefined.test): ...
|
||||
```
|
||||
|
||||
## Class keyword arguments
|
||||
|
||||
### Not deferred in regular files
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
class Foo(metaclass=SomeMeta):
|
||||
pass
|
||||
|
||||
class SomeMeta(type):
|
||||
pass
|
||||
```
|
||||
|
||||
### Deferred in stub files
|
||||
|
||||
Forward references in class keyword arguments are allowed in stub files.
|
||||
|
||||
```pyi
|
||||
class Foo(metaclass=SomeMeta): ...
|
||||
|
||||
class SomeMeta(type): ...
|
||||
```
|
||||
|
||||
### Undefined names are still errors in stub files
|
||||
|
||||
```pyi
|
||||
# error: [unresolved-reference]
|
||||
class Foo(metaclass=NeverDefined): ...
|
||||
```
|
||||
|
||||
## Lambda default argument values
|
||||
|
||||
### Not deferred in regular files
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
f = lambda x=Foo(): x
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
```
|
||||
|
||||
### Deferred in stub files
|
||||
|
||||
Forward references in lambda default argument values are allowed in stub files.
|
||||
|
||||
```pyi
|
||||
f = lambda x=Foo(): x
|
||||
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
### Undefined names are still errors in stub files
|
||||
|
||||
```pyi
|
||||
# error: [unresolved-reference]
|
||||
f = lambda x=NeverDefined(): x
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ python-version = "3.11"
|
||||
```py
|
||||
from typing import Never
|
||||
|
||||
reveal_type(Never) # revealed: <special form 'typing.Never'>
|
||||
reveal_type(Never) # revealed: <special-form 'typing.Never'>
|
||||
```
|
||||
|
||||
### Python 3.10
|
||||
|
||||
@@ -194,7 +194,7 @@ reveal_type(B().name_does_not_matter()) # revealed: B
|
||||
reveal_type(B().positional_only(1)) # revealed: B
|
||||
reveal_type(B().keyword_only(x=1)) # revealed: B
|
||||
# TODO: This should deally be `B`
|
||||
reveal_type(B().decorated_method()) # revealed: Unknown
|
||||
reveal_type(B().decorated_method()) # revealed: Self@decorated_method
|
||||
|
||||
reveal_type(B().a_property) # revealed: B
|
||||
|
||||
|
||||
@@ -152,6 +152,20 @@ The expressions in these string annotations aren't valid expressions in this con
|
||||
shouldn't panic.
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/astral-sh/ty/issues/1865
|
||||
# error: [fstring-type-annotation]
|
||||
stringified_fstring_with_conditional: "f'{1 if 1 else 1}'"
|
||||
# error: [fstring-type-annotation]
|
||||
stringified_fstring_with_boolean_expression: "f'{1 or 2}'"
|
||||
# error: [fstring-type-annotation]
|
||||
stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'"
|
||||
# error: [fstring-type-annotation]
|
||||
stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'"
|
||||
# error: [fstring-type-annotation]
|
||||
stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'"
|
||||
# error: [fstring-type-annotation]
|
||||
stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'"
|
||||
|
||||
a: "1 or 2"
|
||||
b: "(x := 1)"
|
||||
# error: [invalid-type-form]
|
||||
|
||||
@@ -43,9 +43,7 @@ async def main():
|
||||
loop = asyncio.get_event_loop()
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
result = await loop.run_in_executor(pool, blocking_function)
|
||||
|
||||
# TODO: should be `int`
|
||||
reveal_type(result) # revealed: Unknown
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
### `asyncio.Task`
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def _(flag: bool):
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'C1'> | <class 'C1'>`"
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
|
||||
@@ -13,7 +13,7 @@ python-version = "3.10"
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(A | B) # revealed: <types.UnionType special form 'A | B'>
|
||||
reveal_type(A | B) # revealed: <types.UnionType special-form 'A | B'>
|
||||
```
|
||||
|
||||
## Union of two classes (prior to 3.10)
|
||||
@@ -43,14 +43,14 @@ class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(sub_a: type[A], sub_b: type[B]):
|
||||
reveal_type(A | sub_b) # revealed: <types.UnionType special form>
|
||||
reveal_type(sub_a | B) # revealed: <types.UnionType special form>
|
||||
reveal_type(sub_a | sub_b) # revealed: <types.UnionType special form>
|
||||
reveal_type(A | sub_b) # revealed: <types.UnionType special-form>
|
||||
reveal_type(sub_a | B) # revealed: <types.UnionType special-form>
|
||||
reveal_type(sub_a | sub_b) # revealed: <types.UnionType special-form>
|
||||
|
||||
class C[T]: ...
|
||||
class D[T]: ...
|
||||
|
||||
reveal_type(C | D) # revealed: <types.UnionType special form 'C[Unknown] | D[Unknown]'>
|
||||
reveal_type(C | D) # revealed: <types.UnionType special-form 'C[Unknown] | D[Unknown]'>
|
||||
|
||||
reveal_type(C[int] | D[str]) # revealed: <types.UnionType special form 'C[int] | D[str]'>
|
||||
reveal_type(C[int] | D[str]) # revealed: <types.UnionType special-form 'C[int] | D[str]'>
|
||||
```
|
||||
|
||||
@@ -114,6 +114,7 @@ but fall back to `bool` otherwise.
|
||||
```py
|
||||
from enum import Enum
|
||||
from types import FunctionType
|
||||
from typing import TypeVar
|
||||
|
||||
class Answer(Enum):
|
||||
NO = 0
|
||||
@@ -137,6 +138,7 @@ reveal_type(isinstance("", int)) # revealed: bool
|
||||
|
||||
class A: ...
|
||||
class SubclassOfA(A): ...
|
||||
class OtherSubclassOfA(A): ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(isinstance(A, type)) # revealed: Literal[True]
|
||||
@@ -161,6 +163,29 @@ def _(x: A | B, y: list[int]):
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
reveal_type(isinstance(x, B)) # revealed: Literal[True]
|
||||
|
||||
T = TypeVar("T")
|
||||
T_bound_A = TypeVar("T_bound_A", bound=A)
|
||||
T_constrained = TypeVar("T_constrained", SubclassOfA, OtherSubclassOfA)
|
||||
|
||||
def _(
|
||||
x: T,
|
||||
x_bound_a: T_bound_A,
|
||||
x_constrained_sub_a: T_constrained,
|
||||
):
|
||||
reveal_type(isinstance(x, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(x, A)) # revealed: bool
|
||||
|
||||
reveal_type(isinstance(x_bound_a, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(x_bound_a, A)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(x_bound_a, SubclassOfA)) # revealed: bool
|
||||
reveal_type(isinstance(x_bound_a, B)) # revealed: bool
|
||||
|
||||
reveal_type(isinstance(x_constrained_sub_a, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(x_constrained_sub_a, A)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(x_constrained_sub_a, SubclassOfA)) # revealed: bool
|
||||
reveal_type(isinstance(x_constrained_sub_a, OtherSubclassOfA)) # revealed: bool
|
||||
reveal_type(isinstance(x_constrained_sub_a, B)) # revealed: bool
|
||||
```
|
||||
|
||||
Certain special forms in the typing module are not instances of `type`, so are strictly-speaking
|
||||
|
||||
@@ -113,9 +113,7 @@ intention that it shouldn't influence the method's descriptor behavior. For exam
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# TODO: this could use a generic signature, but we don't support
|
||||
# `ParamSpec` and solving of typevars inside `Callable` types yet.
|
||||
def memoize(f: Callable[[C1, int], str]) -> Callable[[C1, int], str]:
|
||||
def memoize[**P, R](f: Callable[P, R]) -> Callable[P, R]:
|
||||
raise NotImplementedError
|
||||
|
||||
class C1:
|
||||
@@ -259,4 +257,37 @@ reveal_type(MyClass().method) # revealed: (...) -> int
|
||||
reveal_type(MyClass().method.__name__) # revealed: str
|
||||
```
|
||||
|
||||
## classmethods passed through Callable-returning decorators
|
||||
|
||||
The behavior described above is also applied to classmethods. If a method is decorated with
|
||||
`@classmethod`, and also with another decorator which returns a Callable type, we make the
|
||||
assumption that the decorator returns a callable which still has the classmethod descriptor
|
||||
behavior.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
return func
|
||||
|
||||
class C:
|
||||
@callable_identity
|
||||
@classmethod
|
||||
def f1(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@classmethod
|
||||
@callable_identity
|
||||
def f2(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
C.f1(C, 1)
|
||||
C.f1(1)
|
||||
C().f1(1)
|
||||
C.f2(1)
|
||||
C().f2(1)
|
||||
```
|
||||
|
||||
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
||||
|
||||
@@ -57,7 +57,7 @@ We can access attributes on objects of all kinds:
|
||||
import sys
|
||||
|
||||
reveal_type(inspect.getattr_static(sys, "dont_write_bytecode")) # revealed: bool
|
||||
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = EllipsisType) -> Any
|
||||
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = ...) -> Any
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static"))
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
@@ -144,7 +144,7 @@ from typing import Any
|
||||
def _(a: Any, tuple_of_any: tuple[Any]):
|
||||
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
|
||||
|
||||
# revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
|
||||
# revealed: def index(self, value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
|
||||
```
|
||||
|
||||
|
||||
@@ -444,20 +444,18 @@ When a `@classmethod` is additionally decorated with another decorator, it is st
|
||||
class method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def does_nothing[T](f: T) -> T:
|
||||
return f
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
def f1(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
def f2(cls, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: str
|
||||
@@ -600,9 +598,9 @@ from typing_extensions import Self
|
||||
|
||||
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type(int.__new__)
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type((42).__new__)
|
||||
|
||||
class X:
|
||||
|
||||
@@ -36,7 +36,7 @@ class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = int, y: int = int) -> Point
|
||||
reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = ..., y: int = ...) -> Point
|
||||
```
|
||||
|
||||
The `__replace__` method can either be called directly or through the `replace` function:
|
||||
|
||||
@@ -602,19 +602,35 @@ super(object, object()).__class__
|
||||
# Not all objects valid in a class's bases list are valid as the first argument to `super()`.
|
||||
# For example, it's valid to inherit from `typing.ChainMap`, but it's not valid as the first argument to `super()`.
|
||||
#
|
||||
# error: [invalid-super-argument] "`<special form 'typing.ChainMap'>` is not a valid class"
|
||||
# error: [invalid-super-argument] "`<special-form 'typing.ChainMap'>` is not a valid class"
|
||||
reveal_type(super(typing.ChainMap, collections.ChainMap())) # revealed: Unknown
|
||||
|
||||
# Meanwhile, it's not valid to inherit from unsubscripted `typing.Generic`,
|
||||
# but it *is* valid as the first argument to `super()`.
|
||||
#
|
||||
# revealed: <super: <special form 'typing.Generic'>, <class 'SupportsInt'>>
|
||||
# revealed: <super: <special-form 'typing.Generic'>, <class 'SupportsInt'>>
|
||||
reveal_type(super(typing.Generic, typing.SupportsInt))
|
||||
|
||||
def _(x: type[typing.Any], y: typing.Any):
|
||||
reveal_type(super(x, y)) # revealed: <super: Any, Any>
|
||||
```
|
||||
|
||||
### Diagnostic when the invalid type is rendered very verbosely
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return False
|
||||
|
||||
def f():
|
||||
if coinflip():
|
||||
class A: ...
|
||||
else:
|
||||
class A: ...
|
||||
super(A, A()) # error: [invalid-super-argument]
|
||||
```
|
||||
|
||||
### Instance Member Access via `super`
|
||||
|
||||
Accessing instance members through `super()` is not allowed.
|
||||
|
||||
@@ -87,25 +87,25 @@ class C:
|
||||
def inner_a(positional=self.a):
|
||||
return
|
||||
self.a = inner_a
|
||||
# revealed: def inner_a(positional=Unknown | (def inner_a(positional=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: def inner_a(positional=...) -> Unknown
|
||||
reveal_type(inner_a)
|
||||
|
||||
def inner_b(*, kw_only=self.b):
|
||||
return
|
||||
self.b = inner_b
|
||||
# revealed: def inner_b(*, kw_only=Unknown | (def inner_b(*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: def inner_b(*, kw_only=...) -> Unknown
|
||||
reveal_type(inner_b)
|
||||
|
||||
def inner_c(positional_only=self.c, /):
|
||||
return
|
||||
self.c = inner_c
|
||||
# revealed: def inner_c(positional_only=Unknown | (def inner_c(positional_only=Unknown, /) -> Unknown), /) -> Unknown
|
||||
# revealed: def inner_c(positional_only=..., /) -> Unknown
|
||||
reveal_type(inner_c)
|
||||
|
||||
def inner_d(*, kw_only=self.d):
|
||||
return
|
||||
self.d = inner_d
|
||||
# revealed: def inner_d(*, kw_only=Unknown | (def inner_d(*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: def inner_d(*, kw_only=...) -> Unknown
|
||||
reveal_type(inner_d)
|
||||
```
|
||||
|
||||
@@ -114,7 +114,7 @@ We do, however, still check assignability of the default value to the parameter
|
||||
```py
|
||||
class D:
|
||||
def f(self: "D"):
|
||||
# error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = Unknown | (def inner_a(a: int = Unknown) -> Unknown)) -> Unknown)` is not assignable to annotated parameter type `int`"
|
||||
# error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = ...) -> Unknown)` is not assignable to annotated parameter type `int`"
|
||||
def inner_a(a: int = self.a): ...
|
||||
self.a = inner_a
|
||||
```
|
||||
@@ -129,16 +129,16 @@ class C:
|
||||
self.c = lambda positional_only=self.c, /: positional_only
|
||||
self.d = lambda *, kw_only=self.d: kw_only
|
||||
|
||||
# revealed: (positional=Unknown | ((positional=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: (positional=...) -> Unknown
|
||||
reveal_type(self.a)
|
||||
|
||||
# revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: (*, kw_only=...) -> Unknown
|
||||
reveal_type(self.b)
|
||||
|
||||
# revealed: (positional_only=Unknown | ((positional_only=Unknown, /) -> Unknown), /) -> Unknown
|
||||
# revealed: (positional_only=..., /) -> Unknown
|
||||
reveal_type(self.c)
|
||||
|
||||
# revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown
|
||||
# revealed: (*, kw_only=...) -> Unknown
|
||||
reveal_type(self.d)
|
||||
```
|
||||
|
||||
|
||||
@@ -643,6 +643,91 @@ reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None
|
||||
Person(name="Alice")
|
||||
```
|
||||
|
||||
### Field specifiers using `**kwargs`
|
||||
|
||||
Some field specifiers may use `**kwargs` to pass through standard parameters like `default`,
|
||||
`default_factory`, `init`, `kw_only`, and `alias`. This section tests that all these parameters work
|
||||
correctly when passed via `**kwargs` for all three kinds of transformers.
|
||||
|
||||
#### Function-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class Person:
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
#### Metaclass-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class Person(ModelBase):
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
#### Base-class-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
class ModelBase: ...
|
||||
|
||||
class Person(ModelBase):
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
### Support for `alias`
|
||||
|
||||
The `alias` parameter in field specifiers allows providing an alternative name for the parameter in
|
||||
@@ -749,8 +834,8 @@ class Outer:
|
||||
outer_a: int = outer_field(init=False)
|
||||
outer_b: str = inner_field(init=False)
|
||||
|
||||
reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = Any) -> None
|
||||
reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = Any) -> None
|
||||
reveal_type(Outer.__init__) # revealed: (self: Outer, outer_b: str = ...) -> None
|
||||
reveal_type(Outer.Inner.__init__) # revealed: (self: Inner, inner_b: str = ...) -> None
|
||||
```
|
||||
|
||||
## Overloaded dataclass-like decorators
|
||||
@@ -868,4 +953,83 @@ reveal_type(t.key) # revealed: int
|
||||
reveal_type(t.name) # revealed: str
|
||||
```
|
||||
|
||||
## `__dataclass_fields__` and `DataclassInstance` protocol
|
||||
|
||||
Classes created via `dataclass_transform` should have `__dataclass_fields__` and
|
||||
`__dataclass_params__` attributes, allowing them to satisfy the `DataclassInstance` protocol. This
|
||||
enables use of `dataclasses.fields`, `dataclasses.asdict`, `dataclasses.replace`, etc.
|
||||
|
||||
### Function-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class Person:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
### Metaclass-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class Person(ModelBase):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
### Base-class-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelBase: ...
|
||||
|
||||
class Person(ModelBase):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
|
||||
@@ -69,7 +69,7 @@ class D:
|
||||
y: str = "default"
|
||||
z: int | None = 1 + 2
|
||||
|
||||
reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
|
||||
reveal_type(D.__init__) # revealed: (self: D, x: int, y: str = "default", z: int | None = 3) -> None
|
||||
```
|
||||
|
||||
This also works if the declaration and binding are split:
|
||||
@@ -221,7 +221,7 @@ class D:
|
||||
(x): int = 1
|
||||
|
||||
# TODO: should ideally not include a `x` parameter
|
||||
reveal_type(D.__init__) # revealed: (self: D, x: int = Literal[1]) -> None
|
||||
reveal_type(D.__init__) # revealed:(self: D, x: int = 1) -> None
|
||||
```
|
||||
|
||||
## `@dataclass` calls with arguments
|
||||
@@ -670,7 +670,7 @@ class A:
|
||||
a: str = field(kw_only=False)
|
||||
b: int = 0
|
||||
|
||||
reveal_type(A.__init__) # revealed: (self: A, a: str, *, b: int = Literal[0]) -> None
|
||||
reveal_type(A.__init__) # revealed:(self: A, a: str, *, b: int = 0) -> None
|
||||
|
||||
A("hi")
|
||||
```
|
||||
@@ -991,7 +991,7 @@ class C:
|
||||
class_variable1: ClassVar[Final[int]] = 1
|
||||
class_variable2: ClassVar[Final[int]] = 1
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, instance_variable_no_default: int, instance_variable: int = Literal[1]) -> None
|
||||
reveal_type(C.__init__) # revealed:(self: C, instance_variable_no_default: int, instance_variable: int = 1) -> None
|
||||
|
||||
c = C(1)
|
||||
# error: [invalid-assignment] "Cannot assign to final attribute `instance_variable` on type `C`"
|
||||
@@ -1082,7 +1082,7 @@ class C(Base):
|
||||
z: int = 10
|
||||
x: int = 15
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
|
||||
reveal_type(C.__init__) # revealed:(self: C, x: int = 15, y: int = 0, z: int = 10) -> None
|
||||
```
|
||||
|
||||
## Conditionally defined fields
|
||||
@@ -1243,7 +1243,7 @@ class UppercaseString:
|
||||
class C:
|
||||
upper: UppercaseString = UppercaseString()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, upper: str = str) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, upper: str = ...) -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.upper) # revealed: str
|
||||
@@ -1289,7 +1289,7 @@ class ConvertToLength:
|
||||
class C:
|
||||
converter: ConvertToLength = ConvertToLength()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, converter: str = Literal[""]) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, converter: str = "") -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.converter) # revealed: int
|
||||
@@ -1328,7 +1328,7 @@ class AcceptsStrAndInt:
|
||||
class C:
|
||||
field: AcceptsStrAndInt = AcceptsStrAndInt()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (self: C, field: str | int = int) -> None
|
||||
reveal_type(C.__init__) # revealed: (self: C, field: str | int = ...) -> None
|
||||
```
|
||||
|
||||
## `dataclasses.field`
|
||||
|
||||
@@ -11,7 +11,7 @@ class Member:
|
||||
role: str = field(default="user")
|
||||
tag: str | None = field(default=None, init=False)
|
||||
|
||||
# revealed: (self: Member, name: str, role: str = Literal["user"]) -> None
|
||||
# revealed: (self: Member, name: str, role: str = "user") -> None
|
||||
reveal_type(Member.__init__)
|
||||
|
||||
alice = Member(name="Alice", role="admin")
|
||||
@@ -37,7 +37,7 @@ class Data:
|
||||
content: list[int] = field(default_factory=list)
|
||||
timestamp: datetime = field(default_factory=datetime.now, init=False)
|
||||
|
||||
# revealed: (self: Data, content: list[int] = list[int]) -> None
|
||||
# revealed: (self: Data, content: list[int] = ...) -> None
|
||||
reveal_type(Data.__init__)
|
||||
|
||||
data = Data([1, 2, 3])
|
||||
@@ -63,7 +63,7 @@ class Person:
|
||||
age: int | None = field(default=None, kw_only=True)
|
||||
role: str = field(default="user", kw_only=True)
|
||||
|
||||
# revealed: (self: Person, name: str, *, age: int | None = None, role: str = Literal["user"]) -> None
|
||||
# revealed: (self: Person, name: str, *, age: int | None = None, role: str = "user") -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
alice = Person(role="admin", name="Alice")
|
||||
@@ -82,8 +82,7 @@ def get_default() -> str:
|
||||
|
||||
reveal_type(field(default=1)) # revealed: dataclasses.Field[Literal[1]]
|
||||
reveal_type(field(default=None)) # revealed: dataclasses.Field[None]
|
||||
# TODO: this could ideally be `dataclasses.Field[str]` with a better generics solver
|
||||
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[Unknown]
|
||||
reveal_type(field(default_factory=get_default)) # revealed: dataclasses.Field[str]
|
||||
```
|
||||
|
||||
## dataclass_transform field_specifiers
|
||||
|
||||
@@ -144,11 +144,10 @@ from functools import cache
|
||||
def f(x: int) -> int:
|
||||
return x**2
|
||||
|
||||
# TODO: Should be `_lru_cache_wrapper[int]`
|
||||
reveal_type(f) # revealed: _lru_cache_wrapper[Unknown]
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(f(1)) # revealed: Unknown
|
||||
# revealed: _lru_cache_wrapper[int]
|
||||
reveal_type(f)
|
||||
# revealed: int
|
||||
reveal_type(f(1))
|
||||
```
|
||||
|
||||
## Lambdas as decorators
|
||||
|
||||
@@ -11,9 +11,9 @@ classes. Uses of these items should subsequently produce a warning.
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@deprecated("use OtherClass")
|
||||
def myfunc(): ...
|
||||
def myfunc(x: int): ...
|
||||
|
||||
myfunc() # error: [deprecated] "use OtherClass"
|
||||
myfunc(1) # error: [deprecated] "use OtherClass"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -195,3 +195,52 @@ class C:
|
||||
c = C()
|
||||
c.square("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Types with the same name but from different files
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import needs_a_foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## TypeVars with bounds that have the same name but are from different files
|
||||
|
||||
In this case, using fully qualified names is *not* necessary.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import needs_a_foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def f[T: Foo](x: T) -> T:
|
||||
needs_a_foo(x) # error: [invalid-argument-type]
|
||||
return x
|
||||
```
|
||||
|
||||
@@ -591,9 +591,9 @@ try:
|
||||
reveal_type(x) # revealed: B | D
|
||||
reveal_type(x) # revealed: B | D
|
||||
x = foo
|
||||
reveal_type(x) # revealed: def foo(param=A) -> Unknown
|
||||
reveal_type(x) # revealed: def foo(param=...) -> Unknown
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1] | (def foo(param=A) -> Unknown)
|
||||
reveal_type(x) # revealed: Literal[1] | (def foo(param=...) -> Unknown)
|
||||
|
||||
class Bar:
|
||||
x = could_raise_returns_E()
|
||||
@@ -603,9 +603,9 @@ except:
|
||||
reveal_type(x) # revealed: <class 'Bar'>
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | <class 'foo'> | <class 'Bar'>`
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | <class 'Bar'>
|
||||
reveal_type(x) # revealed: (def foo(param=...) -> Unknown) | <class 'Bar'>
|
||||
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | <class 'Bar'>
|
||||
reveal_type(x) # revealed: (def foo(param=...) -> Unknown) | <class 'Bar'>
|
||||
```
|
||||
|
||||
[1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d
|
||||
|
||||
@@ -24,8 +24,8 @@ reveal_type(lambda a, b: a + b) # revealed: (a, b) -> Unknown
|
||||
But, it can have default values:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> Unknown
|
||||
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> Unknown
|
||||
reveal_type(lambda a=1: a) # revealed: (a=1) -> Unknown
|
||||
reveal_type(lambda a, b=2: a) # revealed: (a, b=2) -> Unknown
|
||||
```
|
||||
|
||||
And, positional-only parameters:
|
||||
@@ -37,7 +37,7 @@ reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> Unknown
|
||||
And, keyword-only parameters:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> Unknown
|
||||
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=2, c) -> Unknown
|
||||
```
|
||||
|
||||
And, variadic parameter:
|
||||
@@ -55,7 +55,7 @@ reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> Unknown
|
||||
Mixing all of them together:
|
||||
|
||||
```py
|
||||
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> Unknown
|
||||
# revealed: (a, b, /, c=True, *args, *, d="default", e=5, **kwargs) -> Unknown
|
||||
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ Here, a `lambda` expression is used as the default value for a parameter in anot
|
||||
expression.
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=...) -> Unknown
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
@@ -38,7 +38,7 @@ class Product(BaseModel):
|
||||
name: str = Field(..., kw_only=False, min_length=1)
|
||||
internal_price_cent: int = Field(..., gt=0, alias="price_cent")
|
||||
|
||||
reveal_type(Product.__init__) # revealed: (self: Product, name: str = Any, *, price_cent: int = Any) -> None
|
||||
reveal_type(Product.__init__) # revealed: (self: Product, name: str = ..., *, price_cent: int = ...) -> None
|
||||
|
||||
product = Product("Laptop", price_cent=999_00)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class User:
|
||||
id: int
|
||||
role: str = strawberry.field(default="user")
|
||||
|
||||
reveal_type(User.__init__) # revealed: (self: User, *, id: int, role: str = Any) -> None
|
||||
reveal_type(User.__init__) # revealed: (self: User, *, id: int, role: str = ...) -> None
|
||||
|
||||
user = User(id=1)
|
||||
reveal_type(user.id) # revealed: int
|
||||
|
||||
@@ -80,7 +80,7 @@ class Foo(Protocol):
|
||||
def f[T](self, v: T) -> T: ...
|
||||
|
||||
t = (Protocol, int)
|
||||
reveal_type(t[0]) # revealed: <special form 'typing.Protocol'>
|
||||
reveal_type(t[0]) # revealed: <special-form 'typing.Protocol'>
|
||||
|
||||
class Lorem(t[0]):
|
||||
def f(self) -> int: ...
|
||||
|
||||
@@ -555,8 +555,7 @@ def identity(x: T) -> T:
|
||||
def head(xs: list[T]) -> T:
|
||||
return xs[0]
|
||||
|
||||
# TODO: this should be `Literal[1]`
|
||||
reveal_type(invoke(identity, 1)) # revealed: Unknown
|
||||
reveal_type(invoke(identity, 1)) # revealed: Literal[1]
|
||||
|
||||
# TODO: this should be `Unknown | int`
|
||||
reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown
|
||||
|
||||
@@ -518,8 +518,7 @@ V = TypeVar("V", default="V")
|
||||
class D(Generic[V]):
|
||||
x: V
|
||||
|
||||
# TODO: we shouldn't leak a typevar like this in type inference
|
||||
reveal_type(D().x) # revealed: V@D
|
||||
reveal_type(D().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Regression
|
||||
|
||||
@@ -493,8 +493,7 @@ def identity[T](x: T) -> T:
|
||||
def head[T](xs: list[T]) -> T:
|
||||
return xs[0]
|
||||
|
||||
# TODO: this should be `Literal[1]`
|
||||
reveal_type(invoke(identity, 1)) # revealed: Unknown
|
||||
reveal_type(invoke(identity, 1)) # revealed: Literal[1]
|
||||
|
||||
# TODO: this should be `Unknown | int`
|
||||
reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown
|
||||
@@ -736,3 +735,204 @@ def f[T](x: T, y: Not[T]) -> T:
|
||||
y = x # error: [invalid-assignment]
|
||||
return x
|
||||
```
|
||||
|
||||
## `Callable` parameters
|
||||
|
||||
### Class constructors
|
||||
|
||||
We can recurse into the parameters and return values of `Callable` parameters to infer
|
||||
specializations of a generic function.
|
||||
|
||||
```py
|
||||
from typing import Any, Callable, NoReturn, overload, Self
|
||||
from ty_extensions import generic_context, into_callable
|
||||
|
||||
def accepts_callable[**P, R](callable: Callable[P, R]) -> Callable[P, R]:
|
||||
return callable
|
||||
|
||||
def returns_int() -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
# revealed: () -> int
|
||||
reveal_type(into_callable(returns_int))
|
||||
# revealed: () -> int
|
||||
reveal_type(accepts_callable(returns_int))
|
||||
# revealed: int
|
||||
reveal_type(accepts_callable(returns_int)())
|
||||
|
||||
class ClassWithoutConstructor: ...
|
||||
|
||||
# revealed: () -> ClassWithoutConstructor
|
||||
reveal_type(into_callable(ClassWithoutConstructor))
|
||||
# revealed: () -> ClassWithoutConstructor
|
||||
reveal_type(accepts_callable(ClassWithoutConstructor))
|
||||
# revealed: ClassWithoutConstructor
|
||||
reveal_type(accepts_callable(ClassWithoutConstructor)())
|
||||
|
||||
class ClassWithNew:
|
||||
def __new__(cls, *args, **kwargs) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
# revealed: (...) -> ClassWithNew
|
||||
reveal_type(into_callable(ClassWithNew))
|
||||
# revealed: (...) -> ClassWithNew
|
||||
reveal_type(accepts_callable(ClassWithNew))
|
||||
# revealed: ClassWithNew
|
||||
reveal_type(accepts_callable(ClassWithNew)())
|
||||
|
||||
class ClassWithInit:
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
# revealed: () -> ClassWithInit
|
||||
reveal_type(into_callable(ClassWithInit))
|
||||
# revealed: () -> ClassWithInit
|
||||
reveal_type(accepts_callable(ClassWithInit))
|
||||
# revealed: ClassWithInit
|
||||
reveal_type(accepts_callable(ClassWithInit)())
|
||||
|
||||
class ClassWithNewAndInit:
|
||||
def __new__(cls, *args, **kwargs) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, x: int) -> None: ...
|
||||
|
||||
# TODO: We do not currently solve a common behavioral supertype for the two solutions of P.
|
||||
# revealed: ((...) -> ClassWithNewAndInit) | ((x: int) -> ClassWithNewAndInit)
|
||||
reveal_type(into_callable(ClassWithNewAndInit))
|
||||
# TODO: revealed: ((...) -> ClassWithNewAndInit) | ((x: int) -> ClassWithNewAndInit)
|
||||
# revealed: (...) -> ClassWithNewAndInit
|
||||
reveal_type(accepts_callable(ClassWithNewAndInit))
|
||||
# revealed: ClassWithNewAndInit
|
||||
reveal_type(accepts_callable(ClassWithNewAndInit)())
|
||||
|
||||
class Meta(type):
|
||||
def __call__(cls, *args: Any, **kwargs: Any) -> NoReturn:
|
||||
raise NotImplementedError
|
||||
|
||||
class ClassWithNoReturnMetatype(metaclass=Meta):
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO: The return types here are wrong, because we end up creating a constraint (Never ≤ R), which
|
||||
# we confuse with "R has no lower bound".
|
||||
# revealed: (...) -> Never
|
||||
reveal_type(into_callable(ClassWithNoReturnMetatype))
|
||||
# TODO: revealed: (...) -> Never
|
||||
# revealed: (...) -> Unknown
|
||||
reveal_type(accepts_callable(ClassWithNoReturnMetatype))
|
||||
# TODO: revealed: Never
|
||||
# revealed: Unknown
|
||||
reveal_type(accepts_callable(ClassWithNoReturnMetatype)())
|
||||
|
||||
class Proxy: ...
|
||||
|
||||
class ClassWithIgnoredInit:
|
||||
def __new__(cls) -> Proxy:
|
||||
return Proxy()
|
||||
|
||||
def __init__(self, x: int) -> None: ...
|
||||
|
||||
# revealed: () -> Proxy
|
||||
reveal_type(into_callable(ClassWithIgnoredInit))
|
||||
# revealed: () -> Proxy
|
||||
reveal_type(accepts_callable(ClassWithIgnoredInit))
|
||||
# revealed: Proxy
|
||||
reveal_type(accepts_callable(ClassWithIgnoredInit)())
|
||||
|
||||
class ClassWithOverloadedInit[T]:
|
||||
t: T # invariant
|
||||
|
||||
@overload
|
||||
def __init__(self: "ClassWithOverloadedInit[int]", x: int) -> None: ...
|
||||
@overload
|
||||
def __init__(self: "ClassWithOverloadedInit[str]", x: str) -> None: ...
|
||||
def __init__(self, x: int | str) -> None: ...
|
||||
|
||||
# TODO: The old solver cannot handle this overloaded constructor. The ideal solution is that we
|
||||
# would solve **P once, and map it to the entire overloaded signature of the constructor. This
|
||||
# mapping would have to include the return types, since there are different return types for each
|
||||
# overload. We would then also have to determine that R must be equal to the return type of **P's
|
||||
# solution.
|
||||
|
||||
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
|
||||
reveal_type(into_callable(ClassWithOverloadedInit))
|
||||
# TODO: revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
|
||||
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str], (x: str) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]]
|
||||
reveal_type(accepts_callable(ClassWithOverloadedInit))
|
||||
# TODO: revealed: ClassWithOverloadedInit[int]
|
||||
# revealed: ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]
|
||||
reveal_type(accepts_callable(ClassWithOverloadedInit)(0))
|
||||
# TODO: revealed: ClassWithOverloadedInit[str]
|
||||
# revealed: ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]
|
||||
reveal_type(accepts_callable(ClassWithOverloadedInit)(""))
|
||||
|
||||
class GenericClass[T]:
|
||||
t: T # invariant
|
||||
|
||||
def __new__(cls, x: list[T], y: list[T]) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def _(x: list[str]):
|
||||
# TODO: This fails because we are not propagating GenericClass's generic context into the
|
||||
# Callable that we create for it.
|
||||
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
|
||||
reveal_type(into_callable(GenericClass))
|
||||
# revealed: ty_extensions.GenericContext[T@GenericClass]
|
||||
reveal_type(generic_context(into_callable(GenericClass)))
|
||||
|
||||
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
|
||||
reveal_type(accepts_callable(GenericClass))
|
||||
# TODO: revealed: ty_extensions.GenericContext[T@GenericClass]
|
||||
# revealed: None
|
||||
reveal_type(generic_context(accepts_callable(GenericClass)))
|
||||
|
||||
# TODO: revealed: GenericClass[str]
|
||||
# TODO: no errors
|
||||
# revealed: GenericClass[T@GenericClass]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(accepts_callable(GenericClass)(x, x))
|
||||
```
|
||||
|
||||
### Don't include identical lower/upper bounds in type mapping multiple times
|
||||
|
||||
This is was a performance regression reported in
|
||||
[ty#1968](https://github.com/astral-sh/ty/issues/1968). Before fixing this, we would see the
|
||||
`U ≤ M1 | ... | M7` upper bound 7 times. Since we intersect upper bounds before recording a single
|
||||
type mapping, we would perform 7 intersections. Each intersection would require 7^2 comparisons of
|
||||
the `Mx` types. We now have a simple heuristics that avoids processing any identical lower or upper
|
||||
bound more than once, since we know the extra copies cannot affect the result.
|
||||
|
||||
```py
|
||||
from typing import Callable, Generic, TypeVar, Union
|
||||
|
||||
class M1: ...
|
||||
class M2: ...
|
||||
class M3: ...
|
||||
class M4: ...
|
||||
class M5: ...
|
||||
class M6: ...
|
||||
class M7: ...
|
||||
|
||||
Msg = Union[M1, M2, M3, M4, M5, M6, M7]
|
||||
|
||||
T = TypeVar("T")
|
||||
U_co = TypeVar("U_co", covariant=True)
|
||||
|
||||
class Stream(Generic[T]):
|
||||
def apply(self, func: Callable[["Stream[T]"], "Stream[U_co]"]) -> "Stream[U_co]":
|
||||
return func(self)
|
||||
|
||||
TMsg = TypeVar("TMsg", bound=Msg)
|
||||
|
||||
class Builder(Generic[TMsg]):
|
||||
def build(self) -> Stream[TMsg]:
|
||||
stream: Stream[TMsg] = Stream()
|
||||
# TODO: no error
|
||||
# error: [invalid-assignment]
|
||||
stream = stream.apply(self._handler)
|
||||
return stream
|
||||
|
||||
def _handler(self, stream: Stream[Msg]) -> Stream[Msg]:
|
||||
return stream
|
||||
```
|
||||
|
||||
@@ -477,7 +477,7 @@ def keyword_only_with_default_2(*, y: int = 42) -> int:
|
||||
# parameter list i.e., `()`
|
||||
# TODO: This shouldn't error
|
||||
# error: [invalid-argument-type]
|
||||
# revealed: (*, x: int = Literal[42]) -> bool
|
||||
# revealed: (*, x: int = 42) -> bool
|
||||
reveal_type(multiple(keyword_only_with_default_1, keyword_only_with_default_2))
|
||||
|
||||
def keyword_only1(*, x: int) -> int:
|
||||
@@ -503,7 +503,8 @@ class C[**P]:
|
||||
def __init__(self, f: Callable[P, int]) -> None:
|
||||
self.f = f
|
||||
|
||||
def f(x: int, y: str) -> bool:
|
||||
# Note that the return type must match exactly, since C is invariant on the return type of C.f.
|
||||
def f(x: int, y: str) -> int:
|
||||
return True
|
||||
|
||||
c = C(f)
|
||||
@@ -618,6 +619,22 @@ reveal_type(foo.method) # revealed: bound method Foo[(int, str, /)].method(int,
|
||||
reveal_type(foo.method(1, "a")) # revealed: str
|
||||
```
|
||||
|
||||
### Gradual types propagate through `ParamSpec` inference
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
return func
|
||||
|
||||
@callable_identity
|
||||
def f(env: dict) -> None:
|
||||
pass
|
||||
|
||||
# revealed: (env: dict[Unknown, Unknown]) -> None
|
||||
reveal_type(f)
|
||||
```
|
||||
|
||||
### Overloads
|
||||
|
||||
`overloaded.pyi`:
|
||||
@@ -662,7 +679,7 @@ reveal_type(change_return_type(int_int)) # revealed: Overload[(x: int) -> str,
|
||||
reveal_type(change_return_type(int_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(change_return_type(str_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
|
||||
reveal_type(change_return_type(str_str)) # revealed: (...) -> str
|
||||
|
||||
# TODO: Both of these shouldn't raise an error
|
||||
# error: [invalid-argument-type]
|
||||
|
||||
@@ -883,7 +883,7 @@ reveal_type(C[int]().y) # revealed: int
|
||||
class D[T = T]:
|
||||
x: T
|
||||
|
||||
reveal_type(D().x) # revealed: T@D
|
||||
reveal_type(D().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
[pep 695]: https://peps.python.org/pep-0695/
|
||||
|
||||
@@ -426,7 +426,8 @@ from ty_extensions import ConstraintSet, generic_context
|
||||
def mentions[T, U]():
|
||||
# (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions])
|
||||
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T])
|
||||
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
|
||||
# TODO: revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
|
||||
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = Unknown]
|
||||
reveal_type(generic_context(mentions).specialize_constrained(constraints))
|
||||
```
|
||||
|
||||
|
||||
@@ -77,44 +77,44 @@ IntOrTypeVar = int | T
|
||||
TypeVarOrNone = T | None
|
||||
NoneOrTypeVar = None | T
|
||||
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(IntOrStrOrBytes1) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes2) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes3) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes4) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes5) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes6) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(BytesOrIntOrStr) # revealed: <types.UnionType special form 'bytes | int | str'>
|
||||
reveal_type(IntOrNone) # revealed: <types.UnionType special form 'int | None'>
|
||||
reveal_type(NoneOrInt) # revealed: <types.UnionType special form 'None | int'>
|
||||
reveal_type(IntOrStrOrNone) # revealed: <types.UnionType special form 'int | str | None'>
|
||||
reveal_type(NoneOrIntOrStr) # revealed: <types.UnionType special form 'None | int | str'>
|
||||
reveal_type(IntOrAny) # revealed: <types.UnionType special form 'int | Any'>
|
||||
reveal_type(AnyOrInt) # revealed: <types.UnionType special form 'Any | int'>
|
||||
reveal_type(NoneOrAny) # revealed: <types.UnionType special form 'None | Any'>
|
||||
reveal_type(AnyOrNone) # revealed: <types.UnionType special form 'Any | None'>
|
||||
reveal_type(NeverOrAny) # revealed: <types.UnionType special form 'Any'>
|
||||
reveal_type(AnyOrNever) # revealed: <types.UnionType special form 'Any'>
|
||||
reveal_type(UnknownOrInt) # revealed: <types.UnionType special form 'Unknown | int'>
|
||||
reveal_type(IntOrUnknown) # revealed: <types.UnionType special form 'int | Unknown'>
|
||||
reveal_type(StrOrZero) # revealed: <types.UnionType special form 'str | Literal[0]'>
|
||||
reveal_type(ZeroOrStr) # revealed: <types.UnionType special form 'Literal[0] | str'>
|
||||
reveal_type(IntOrLiteralString) # revealed: <types.UnionType special form 'int | LiteralString'>
|
||||
reveal_type(LiteralStringOrInt) # revealed: <types.UnionType special form 'LiteralString | int'>
|
||||
reveal_type(NoneOrTuple) # revealed: <types.UnionType special form 'None | tuple[int, str]'>
|
||||
reveal_type(TupleOrNone) # revealed: <types.UnionType special form 'tuple[int, str] | None'>
|
||||
reveal_type(IntOrAnnotated) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(AnnotatedOrInt) # revealed: <types.UnionType special form 'str | int'>
|
||||
reveal_type(IntOrOptional) # revealed: <types.UnionType special form 'int | str | None'>
|
||||
reveal_type(OptionalOrInt) # revealed: <types.UnionType special form 'str | None | int'>
|
||||
reveal_type(IntOrTypeOfStr) # revealed: <types.UnionType special form 'int | type[str]'>
|
||||
reveal_type(TypeOfStrOrInt) # revealed: <types.UnionType special form 'type[str] | int'>
|
||||
reveal_type(IntOrCallable) # revealed: <types.UnionType special form 'int | ((str, /) -> bytes)'>
|
||||
reveal_type(CallableOrInt) # revealed: <types.UnionType special form '((str, /) -> bytes) | int'>
|
||||
reveal_type(TypeVarOrInt) # revealed: <types.UnionType special form 'T@TypeVarOrInt | int'>
|
||||
reveal_type(IntOrTypeVar) # revealed: <types.UnionType special form 'int | T@IntOrTypeVar'>
|
||||
reveal_type(TypeVarOrNone) # revealed: <types.UnionType special form 'T@TypeVarOrNone | None'>
|
||||
reveal_type(NoneOrTypeVar) # revealed: <types.UnionType special form 'None | T@NoneOrTypeVar'>
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special-form 'int | str'>
|
||||
reveal_type(IntOrStrOrBytes1) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes2) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes3) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes4) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes5) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
reveal_type(IntOrStrOrBytes6) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
reveal_type(BytesOrIntOrStr) # revealed: <types.UnionType special-form 'bytes | int | str'>
|
||||
reveal_type(IntOrNone) # revealed: <types.UnionType special-form 'int | None'>
|
||||
reveal_type(NoneOrInt) # revealed: <types.UnionType special-form 'None | int'>
|
||||
reveal_type(IntOrStrOrNone) # revealed: <types.UnionType special-form 'int | str | None'>
|
||||
reveal_type(NoneOrIntOrStr) # revealed: <types.UnionType special-form 'None | int | str'>
|
||||
reveal_type(IntOrAny) # revealed: <types.UnionType special-form 'int | Any'>
|
||||
reveal_type(AnyOrInt) # revealed: <types.UnionType special-form 'Any | int'>
|
||||
reveal_type(NoneOrAny) # revealed: <types.UnionType special-form 'None | Any'>
|
||||
reveal_type(AnyOrNone) # revealed: <types.UnionType special-form 'Any | None'>
|
||||
reveal_type(NeverOrAny) # revealed: <types.UnionType special-form 'Any'>
|
||||
reveal_type(AnyOrNever) # revealed: <types.UnionType special-form 'Any'>
|
||||
reveal_type(UnknownOrInt) # revealed: <types.UnionType special-form 'Unknown | int'>
|
||||
reveal_type(IntOrUnknown) # revealed: <types.UnionType special-form 'int | Unknown'>
|
||||
reveal_type(StrOrZero) # revealed: <types.UnionType special-form 'str | Literal[0]'>
|
||||
reveal_type(ZeroOrStr) # revealed: <types.UnionType special-form 'Literal[0] | str'>
|
||||
reveal_type(IntOrLiteralString) # revealed: <types.UnionType special-form 'int | LiteralString'>
|
||||
reveal_type(LiteralStringOrInt) # revealed: <types.UnionType special-form 'LiteralString | int'>
|
||||
reveal_type(NoneOrTuple) # revealed: <types.UnionType special-form 'None | tuple[int, str]'>
|
||||
reveal_type(TupleOrNone) # revealed: <types.UnionType special-form 'tuple[int, str] | None'>
|
||||
reveal_type(IntOrAnnotated) # revealed: <types.UnionType special-form 'int | str'>
|
||||
reveal_type(AnnotatedOrInt) # revealed: <types.UnionType special-form 'str | int'>
|
||||
reveal_type(IntOrOptional) # revealed: <types.UnionType special-form 'int | str | None'>
|
||||
reveal_type(OptionalOrInt) # revealed: <types.UnionType special-form 'str | None | int'>
|
||||
reveal_type(IntOrTypeOfStr) # revealed: <types.UnionType special-form 'int | type[str]'>
|
||||
reveal_type(TypeOfStrOrInt) # revealed: <types.UnionType special-form 'type[str] | int'>
|
||||
reveal_type(IntOrCallable) # revealed: <types.UnionType special-form 'int | ((str, /) -> bytes)'>
|
||||
reveal_type(CallableOrInt) # revealed: <types.UnionType special-form '((str, /) -> bytes) | int'>
|
||||
reveal_type(TypeVarOrInt) # revealed: <types.UnionType special-form 'T@TypeVarOrInt | int'>
|
||||
reveal_type(IntOrTypeVar) # revealed: <types.UnionType special-form 'int | T@IntOrTypeVar'>
|
||||
reveal_type(TypeVarOrNone) # revealed: <types.UnionType special-form 'T@TypeVarOrNone | None'>
|
||||
reveal_type(NoneOrTypeVar) # revealed: <types.UnionType special-form 'None | T@NoneOrTypeVar'>
|
||||
|
||||
def _(
|
||||
int_or_str: IntOrStr,
|
||||
@@ -295,7 +295,7 @@ X = Foo | Bar
|
||||
# In an ideal world, perhaps we would respect `Meta.__or__` here and reveal `str`?
|
||||
# But we still need to record what the elements are, since (according to the typing spec)
|
||||
# `X` is still a valid type alias
|
||||
reveal_type(X) # revealed: <types.UnionType special form 'Foo | Bar'>
|
||||
reveal_type(X) # revealed: <types.UnionType special-form 'Foo | Bar'>
|
||||
|
||||
def f(obj: X):
|
||||
reveal_type(obj) # revealed: Foo | Bar
|
||||
@@ -391,17 +391,17 @@ MyOptional = T | None
|
||||
|
||||
reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
|
||||
reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
|
||||
reveal_type(MyType) # revealed: <special form 'type[T@MyType]'>
|
||||
reveal_type(MyType) # revealed: <special-form 'type[T@MyType]'>
|
||||
reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
|
||||
reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
|
||||
reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
|
||||
reveal_type(ListOrTuple) # revealed: <types.UnionType special form 'list[T@ListOrTuple] | tuple[T@ListOrTuple, ...]'>
|
||||
# revealed: <types.UnionType special form 'list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...]'>
|
||||
reveal_type(ListOrTuple) # revealed: <types.UnionType special-form 'list[T@ListOrTuple] | tuple[T@ListOrTuple, ...]'>
|
||||
# revealed: <types.UnionType special-form 'list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...]'>
|
||||
reveal_type(ListOrTupleLegacy)
|
||||
reveal_type(MyCallable) # revealed: <typing.Callable special form '(**P@MyCallable) -> T@MyCallable'>
|
||||
reveal_type(AnnotatedType) # revealed: <special form 'typing.Annotated[T@AnnotatedType, <metadata>]'>
|
||||
reveal_type(MyCallable) # revealed: <typing.Callable special-form '(**P@MyCallable) -> T@MyCallable'>
|
||||
reveal_type(AnnotatedType) # revealed: <special-form 'typing.Annotated[T@AnnotatedType, <metadata>]'>
|
||||
reveal_type(TransparentAlias) # revealed: TypeVar
|
||||
reveal_type(MyOptional) # revealed: <types.UnionType special form 'T@MyOptional | None'>
|
||||
reveal_type(MyOptional) # revealed: <types.UnionType special-form 'T@MyOptional | None'>
|
||||
|
||||
def _(
|
||||
list_of_ints: MyList[int],
|
||||
@@ -457,13 +457,13 @@ AnnotatedInt = AnnotatedType[int]
|
||||
SubclassOfInt = MyType[int]
|
||||
CallableIntToStr = MyCallable[[int], str]
|
||||
|
||||
reveal_type(IntsOrNone) # revealed: <types.UnionType special form 'list[int] | None'>
|
||||
reveal_type(IntsOrStrs) # revealed: <types.UnionType special form 'tuple[int, int] | tuple[str, str]'>
|
||||
reveal_type(IntsOrNone) # revealed: <types.UnionType special-form 'list[int] | None'>
|
||||
reveal_type(IntsOrStrs) # revealed: <types.UnionType special-form 'tuple[int, int] | tuple[str, str]'>
|
||||
reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
|
||||
reveal_type(ListOrTupleOfInts) # revealed: <types.UnionType special form 'list[int] | tuple[int, ...]'>
|
||||
reveal_type(AnnotatedInt) # revealed: <special form 'typing.Annotated[int, <metadata>]'>
|
||||
reveal_type(SubclassOfInt) # revealed: <special form 'type[int]'>
|
||||
reveal_type(CallableIntToStr) # revealed: <typing.Callable special form '(int, /) -> str'>
|
||||
reveal_type(ListOrTupleOfInts) # revealed: <types.UnionType special-form 'list[int] | tuple[int, ...]'>
|
||||
reveal_type(AnnotatedInt) # revealed: <special-form 'typing.Annotated[int, <metadata>]'>
|
||||
reveal_type(SubclassOfInt) # revealed: <special-form 'type[int]'>
|
||||
reveal_type(CallableIntToStr) # revealed: <typing.Callable special-form '(int, /) -> str'>
|
||||
|
||||
def _(
|
||||
ints_or_none: IntsOrNone,
|
||||
@@ -495,8 +495,8 @@ MyOtherType = MyType[T]
|
||||
TypeOrList = MyType[B] | MyList[B]
|
||||
|
||||
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
|
||||
reveal_type(MyOtherType) # revealed: <special form 'type[T@MyOtherType]'>
|
||||
reveal_type(TypeOrList) # revealed: <types.UnionType special form 'type[B@TypeOrList] | list[B@TypeOrList]'>
|
||||
reveal_type(MyOtherType) # revealed: <special-form 'type[T@MyOtherType]'>
|
||||
reveal_type(TypeOrList) # revealed: <types.UnionType special-form 'type[B@TypeOrList] | list[B@TypeOrList]'>
|
||||
|
||||
def _(
|
||||
list_of_ints: MyOtherList[int],
|
||||
@@ -730,7 +730,7 @@ def _(doubly_specialized: DictInt[int]):
|
||||
|
||||
Union = list[str] | list[int]
|
||||
|
||||
# error: [non-subscriptable] "Cannot subscript non-generic type: `<types.UnionType special form 'list[str] | list[int]'>` is already specialized"
|
||||
# error: [non-subscriptable] "Cannot subscript non-generic type: `<types.UnionType special-form 'list[str] | list[int]'>` is already specialized"
|
||||
def _(doubly_specialized: Union[int]):
|
||||
reveal_type(doubly_specialized) # revealed: Unknown
|
||||
|
||||
@@ -974,7 +974,7 @@ from typing import Optional
|
||||
|
||||
MyOptionalInt = Optional[int]
|
||||
|
||||
reveal_type(MyOptionalInt) # revealed: <types.UnionType special form 'int | None'>
|
||||
reveal_type(MyOptionalInt) # revealed: <types.UnionType special-form 'int | None'>
|
||||
|
||||
def _(optional_int: MyOptionalInt):
|
||||
reveal_type(optional_int) # revealed: int | None
|
||||
@@ -1007,9 +1007,9 @@ MyLiteralString = LiteralString
|
||||
MyNoReturn = NoReturn
|
||||
MyNever = Never
|
||||
|
||||
reveal_type(MyLiteralString) # revealed: <special form 'typing.LiteralString'>
|
||||
reveal_type(MyNoReturn) # revealed: <special form 'typing.NoReturn'>
|
||||
reveal_type(MyNever) # revealed: <special form 'typing.Never'>
|
||||
reveal_type(MyLiteralString) # revealed: <special-form 'typing.LiteralString'>
|
||||
reveal_type(MyNoReturn) # revealed: <special-form 'typing.NoReturn'>
|
||||
reveal_type(MyNever) # revealed: <special-form 'typing.Never'>
|
||||
|
||||
def _(
|
||||
ls: MyLiteralString,
|
||||
@@ -1062,8 +1062,8 @@ from typing import Union
|
||||
IntOrStr = Union[int, str]
|
||||
IntOrStrOrBytes = Union[int, Union[str, bytes]]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(IntOrStrOrBytes) # revealed: <types.UnionType special form 'int | str | bytes'>
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special-form 'int | str'>
|
||||
reveal_type(IntOrStrOrBytes) # revealed: <types.UnionType special-form 'int | str | bytes'>
|
||||
|
||||
def _(
|
||||
int_or_str: IntOrStr,
|
||||
@@ -1091,7 +1091,7 @@ An empty `typing.Union` leads to a `TypeError` at runtime, so we emit an error.
|
||||
# error: [invalid-type-form] "`typing.Union` requires at least one type argument"
|
||||
EmptyUnion = Union[()]
|
||||
|
||||
reveal_type(EmptyUnion) # revealed: <types.UnionType special form 'Never'>
|
||||
reveal_type(EmptyUnion) # revealed: <types.UnionType special-form 'Never'>
|
||||
|
||||
def _(empty: EmptyUnion):
|
||||
reveal_type(empty) # revealed: Never
|
||||
@@ -1136,14 +1136,14 @@ SubclassOfG = type[G]
|
||||
SubclassOfGInt = type[G[int]]
|
||||
SubclassOfP = type[P]
|
||||
|
||||
reveal_type(SubclassOfA) # revealed: <special form 'type[A]'>
|
||||
reveal_type(SubclassOfAny) # revealed: <special form 'type[Any]'>
|
||||
reveal_type(SubclassOfAOrB1) # revealed: <special form 'type[A | B]'>
|
||||
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfG) # revealed: <special form 'type[G[Unknown]]'>
|
||||
reveal_type(SubclassOfGInt) # revealed: <special form 'type[G[int]]'>
|
||||
reveal_type(SubclassOfP) # revealed: <special form 'type[P]'>
|
||||
reveal_type(SubclassOfA) # revealed: <special-form 'type[A]'>
|
||||
reveal_type(SubclassOfAny) # revealed: <special-form 'type[Any]'>
|
||||
reveal_type(SubclassOfAOrB1) # revealed: <special-form 'type[A | B]'>
|
||||
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfG) # revealed: <special-form 'type[G[Unknown]]'>
|
||||
reveal_type(SubclassOfGInt) # revealed: <special-form 'type[G[int]]'>
|
||||
reveal_type(SubclassOfP) # revealed: <special-form 'type[P]'>
|
||||
|
||||
def _(
|
||||
subclass_of_a: SubclassOfA,
|
||||
@@ -1224,14 +1224,14 @@ SubclassOfG = Type[G]
|
||||
SubclassOfGInt = Type[G[int]]
|
||||
SubclassOfP = Type[P]
|
||||
|
||||
reveal_type(SubclassOfA) # revealed: <special form 'type[A]'>
|
||||
reveal_type(SubclassOfAny) # revealed: <special form 'type[Any]'>
|
||||
reveal_type(SubclassOfAOrB1) # revealed: <special form 'type[A | B]'>
|
||||
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfG) # revealed: <special form 'type[G[Unknown]]'>
|
||||
reveal_type(SubclassOfGInt) # revealed: <special form 'type[G[int]]'>
|
||||
reveal_type(SubclassOfP) # revealed: <special form 'type[P]'>
|
||||
reveal_type(SubclassOfA) # revealed: <special-form 'type[A]'>
|
||||
reveal_type(SubclassOfAny) # revealed: <special-form 'type[Any]'>
|
||||
reveal_type(SubclassOfAOrB1) # revealed: <special-form 'type[A | B]'>
|
||||
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
|
||||
reveal_type(SubclassOfG) # revealed: <special-form 'type[G[Unknown]]'>
|
||||
reveal_type(SubclassOfGInt) # revealed: <special-form 'type[G[int]]'>
|
||||
reveal_type(SubclassOfP) # revealed: <special-form 'type[P]'>
|
||||
|
||||
def _(
|
||||
subclass_of_a: SubclassOfA,
|
||||
@@ -1346,25 +1346,25 @@ DefaultDictOrNone = DefaultDict[str, int] | None
|
||||
DequeOrNone = Deque[str] | None
|
||||
OrderedDictOrNone = OrderedDict[str, int] | None
|
||||
|
||||
reveal_type(NoneOrList) # revealed: <types.UnionType special form 'None | list[str]'>
|
||||
reveal_type(NoneOrSet) # revealed: <types.UnionType special form 'None | set[str]'>
|
||||
reveal_type(NoneOrDict) # revealed: <types.UnionType special form 'None | dict[str, int]'>
|
||||
reveal_type(NoneOrFrozenSet) # revealed: <types.UnionType special form 'None | frozenset[str]'>
|
||||
reveal_type(NoneOrChainMap) # revealed: <types.UnionType special form 'None | ChainMap[str, int]'>
|
||||
reveal_type(NoneOrCounter) # revealed: <types.UnionType special form 'None | Counter[str]'>
|
||||
reveal_type(NoneOrDefaultDict) # revealed: <types.UnionType special form 'None | defaultdict[str, int]'>
|
||||
reveal_type(NoneOrDeque) # revealed: <types.UnionType special form 'None | deque[str]'>
|
||||
reveal_type(NoneOrOrderedDict) # revealed: <types.UnionType special form 'None | OrderedDict[str, int]'>
|
||||
reveal_type(NoneOrList) # revealed: <types.UnionType special-form 'None | list[str]'>
|
||||
reveal_type(NoneOrSet) # revealed: <types.UnionType special-form 'None | set[str]'>
|
||||
reveal_type(NoneOrDict) # revealed: <types.UnionType special-form 'None | dict[str, int]'>
|
||||
reveal_type(NoneOrFrozenSet) # revealed: <types.UnionType special-form 'None | frozenset[str]'>
|
||||
reveal_type(NoneOrChainMap) # revealed: <types.UnionType special-form 'None | ChainMap[str, int]'>
|
||||
reveal_type(NoneOrCounter) # revealed: <types.UnionType special-form 'None | Counter[str]'>
|
||||
reveal_type(NoneOrDefaultDict) # revealed: <types.UnionType special-form 'None | defaultdict[str, int]'>
|
||||
reveal_type(NoneOrDeque) # revealed: <types.UnionType special-form 'None | deque[str]'>
|
||||
reveal_type(NoneOrOrderedDict) # revealed: <types.UnionType special-form 'None | OrderedDict[str, int]'>
|
||||
|
||||
reveal_type(ListOrNone) # revealed: <types.UnionType special form 'list[int] | None'>
|
||||
reveal_type(SetOrNone) # revealed: <types.UnionType special form 'set[int] | None'>
|
||||
reveal_type(DictOrNone) # revealed: <types.UnionType special form 'dict[str, int] | None'>
|
||||
reveal_type(FrozenSetOrNone) # revealed: <types.UnionType special form 'frozenset[int] | None'>
|
||||
reveal_type(ChainMapOrNone) # revealed: <types.UnionType special form 'ChainMap[str, int] | None'>
|
||||
reveal_type(CounterOrNone) # revealed: <types.UnionType special form 'Counter[str] | None'>
|
||||
reveal_type(DefaultDictOrNone) # revealed: <types.UnionType special form 'defaultdict[str, int] | None'>
|
||||
reveal_type(DequeOrNone) # revealed: <types.UnionType special form 'deque[str] | None'>
|
||||
reveal_type(OrderedDictOrNone) # revealed: <types.UnionType special form 'OrderedDict[str, int] | None'>
|
||||
reveal_type(ListOrNone) # revealed: <types.UnionType special-form 'list[int] | None'>
|
||||
reveal_type(SetOrNone) # revealed: <types.UnionType special-form 'set[int] | None'>
|
||||
reveal_type(DictOrNone) # revealed: <types.UnionType special-form 'dict[str, int] | None'>
|
||||
reveal_type(FrozenSetOrNone) # revealed: <types.UnionType special-form 'frozenset[int] | None'>
|
||||
reveal_type(ChainMapOrNone) # revealed: <types.UnionType special-form 'ChainMap[str, int] | None'>
|
||||
reveal_type(CounterOrNone) # revealed: <types.UnionType special-form 'Counter[str] | None'>
|
||||
reveal_type(DefaultDictOrNone) # revealed: <types.UnionType special-form 'defaultdict[str, int] | None'>
|
||||
reveal_type(DequeOrNone) # revealed: <types.UnionType special-form 'deque[str] | None'>
|
||||
reveal_type(OrderedDictOrNone) # revealed: <types.UnionType special-form 'OrderedDict[str, int] | None'>
|
||||
|
||||
def _(
|
||||
none_or_list: NoneOrList,
|
||||
@@ -1457,9 +1457,9 @@ CallableNoArgs = Callable[[], None]
|
||||
BasicCallable = Callable[[int, str], bytes]
|
||||
GradualCallable = Callable[..., str]
|
||||
|
||||
reveal_type(CallableNoArgs) # revealed: <typing.Callable special form '() -> None'>
|
||||
reveal_type(BasicCallable) # revealed: <typing.Callable special form '(int, str, /) -> bytes'>
|
||||
reveal_type(GradualCallable) # revealed: <typing.Callable special form '(...) -> str'>
|
||||
reveal_type(CallableNoArgs) # revealed: <typing.Callable special-form '() -> None'>
|
||||
reveal_type(BasicCallable) # revealed: <typing.Callable special-form '(int, str, /) -> bytes'>
|
||||
reveal_type(GradualCallable) # revealed: <typing.Callable special-form '(...) -> str'>
|
||||
|
||||
def _(
|
||||
callable_no_args: CallableNoArgs,
|
||||
@@ -1491,8 +1491,8 @@ InvalidCallable1 = Callable[[int]]
|
||||
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
InvalidCallable2 = Callable[int, str]
|
||||
|
||||
reveal_type(InvalidCallable1) # revealed: <typing.Callable special form '(...) -> Unknown'>
|
||||
reveal_type(InvalidCallable2) # revealed: <typing.Callable special form '(...) -> Unknown'>
|
||||
reveal_type(InvalidCallable1) # revealed: <typing.Callable special-form '(...) -> Unknown'>
|
||||
reveal_type(InvalidCallable2) # revealed: <typing.Callable special-form '(...) -> Unknown'>
|
||||
|
||||
def _(invalid_callable1: InvalidCallable1, invalid_callable2: InvalidCallable2):
|
||||
reveal_type(invalid_callable1) # revealed: (...) -> Unknown
|
||||
|
||||
@@ -53,8 +53,8 @@ in `import os.path as os.path` the `os.path` is not a valid identifier.
|
||||
```py
|
||||
from b import Any, Literal, foo
|
||||
|
||||
reveal_type(Any) # revealed: <special form 'typing.Any'>
|
||||
reveal_type(Literal) # revealed: <special form 'typing.Literal'>
|
||||
reveal_type(Any) # revealed: <special-form 'typing.Any'>
|
||||
reveal_type(Literal) # revealed: <special-form 'typing.Literal'>
|
||||
reveal_type(foo) # revealed: <module 'foo'>
|
||||
```
|
||||
|
||||
@@ -132,7 +132,7 @@ reveal_type(Any) # revealed: Unknown
|
||||
```pyi
|
||||
from typing import Any
|
||||
|
||||
reveal_type(Any) # revealed: <special form 'typing.Any'>
|
||||
reveal_type(Any) # revealed: <special-form 'typing.Any'>
|
||||
```
|
||||
|
||||
## Nested mixed re-export and not
|
||||
@@ -169,7 +169,7 @@ reveal_type(Any) # revealed: Unknown
|
||||
```pyi
|
||||
from typing import Any
|
||||
|
||||
reveal_type(Any) # revealed: <special form 'typing.Any'>
|
||||
reveal_type(Any) # revealed: <special-form 'typing.Any'>
|
||||
```
|
||||
|
||||
## Exported as different name
|
||||
|
||||
@@ -1437,7 +1437,7 @@ are present due to `*` imports.
|
||||
import collections.abc
|
||||
|
||||
reveal_type(collections.abc.Sequence) # revealed: <class 'Sequence'>
|
||||
reveal_type(collections.abc.Callable) # revealed: <special form 'typing.Callable'>
|
||||
reveal_type(collections.abc.Callable) # revealed: <special-form 'typing.Callable'>
|
||||
reveal_type(collections.abc.Set) # revealed: <class 'AbstractSet'>
|
||||
```
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ x11: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3]
|
||||
reveal_type(x11) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
x12: Y[Y[Literal[1]]] = [[1]]
|
||||
reveal_type(x12) # revealed: list[Y[Literal[1]]]
|
||||
reveal_type(x12) # revealed: list[list[Literal[1]]]
|
||||
|
||||
x13: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
|
||||
reveal_type(x13) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]
|
||||
|
||||
@@ -289,6 +289,14 @@ reveal_type(x) # revealed: <class 'A'> | <class 'B'>
|
||||
class Foo(x): ...
|
||||
|
||||
reveal_mro(Foo) # revealed: (<class 'Foo'>, Unknown, <class 'object'>)
|
||||
|
||||
def f():
|
||||
if returns_bool():
|
||||
class C: ...
|
||||
else:
|
||||
class C: ...
|
||||
|
||||
class D(C): ... # error: [unsupported-base]
|
||||
```
|
||||
|
||||
## `UnionType` instances are now allowed as a base
|
||||
@@ -301,7 +309,7 @@ class B: ...
|
||||
|
||||
EitherOr = A | B
|
||||
|
||||
# error: [invalid-base] "Invalid class base with type `<types.UnionType special form 'A | B'>`"
|
||||
# error: [invalid-base] "Invalid class base with type `<types.UnionType special-form 'A | B'>`"
|
||||
class Foo(EitherOr): ...
|
||||
```
|
||||
|
||||
@@ -393,7 +401,7 @@ else:
|
||||
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
reveal_mro(B)
|
||||
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)
|
||||
|
||||
@@ -104,6 +104,8 @@ class C:
|
||||
value: str | None
|
||||
|
||||
def foo(c: C):
|
||||
# The truthiness check `c.value` narrows to `str & ~AlwaysFalsy`.
|
||||
# The subsequent `len(c.value)` doesn't narrow further since `str` is not narrowable by len().
|
||||
if c.value and len(c.value):
|
||||
reveal_type(c.value) # revealed: str & ~AlwaysFalsy
|
||||
|
||||
@@ -114,7 +116,7 @@ def foo(c: C):
|
||||
if c.value is None or not len(c.value):
|
||||
reveal_type(c.value) # revealed: str | None
|
||||
else: # c.value is not None and len(c.value)
|
||||
# TODO: should be # `str & ~AlwaysFalsy`
|
||||
# `c.value is not None` narrows to `str`, but `str` is not narrowable by len().
|
||||
reveal_type(c.value) # revealed: str
|
||||
```
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ from typing import Union
|
||||
|
||||
IntOrStr = Union[int, str]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special-form 'int | str'>
|
||||
|
||||
def _(x: int | str | bytes | memoryview | range):
|
||||
if isinstance(x, IntOrStr):
|
||||
|
||||
@@ -209,7 +209,7 @@ from typing import Union
|
||||
|
||||
IntOrStr = Union[int, str]
|
||||
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special form 'int | str'>
|
||||
reveal_type(IntOrStr) # revealed: <types.UnionType special-form 'int | str'>
|
||||
|
||||
def f(x: type[int | str | bytes | range]):
|
||||
if issubclass(x, IntOrStr):
|
||||
|
||||
131
crates/ty_python_semantic/resources/mdtest/narrow/len.md
Normal file
131
crates/ty_python_semantic/resources/mdtest/narrow/len.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Narrowing for `len(..)` checks
|
||||
|
||||
When `len(x)` is used in a boolean context, we can narrow the type of `x` based on whether `len(x)`
|
||||
is truthy (non-zero) or falsy (zero).
|
||||
|
||||
We apply `~AlwaysFalsy` narrowing when ANY part of the type is narrowable (string/bytes literals,
|
||||
`LiteralString`, tuples). This removes types that are always falsy (like `Literal[""]`) while
|
||||
leaving non-narrowable types (like `str`, `list`) unchanged.
|
||||
|
||||
## String literals
|
||||
|
||||
The intersection with `~AlwaysFalsy` simplifies to just the non-empty literal.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal["foo", ""]):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: Literal["foo"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Bytes literals
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal[b"foo", b""]):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: Literal[b"foo"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[b""]
|
||||
```
|
||||
|
||||
## LiteralString
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
|
||||
def _(x: LiteralString):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal[""]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
Ideally we'd narrow these types further, e.g. to `tuple[int, ...] & ~tuple[()]` in the positive case
|
||||
and `tuple[()]` in the negative case (see <https://github.com/astral-sh/ty/issues/560>).
|
||||
|
||||
```py
|
||||
def _(x: tuple[int, ...]):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: tuple[int, ...] & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[int, ...] & ~AlwaysTruthy
|
||||
```
|
||||
|
||||
## Unions of narrowable types
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal["foo", ""] | tuple[int, ...]):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: Literal["foo"] | (tuple[int, ...] & ~AlwaysFalsy)
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[""] | (tuple[int, ...] & ~AlwaysTruthy)
|
||||
```
|
||||
|
||||
## Types that are not narrowed
|
||||
|
||||
For `str`, `list`, and other types where a subclass could have a `__bool__` that disagrees with
|
||||
`__len__`, we do not narrow:
|
||||
|
||||
```py
|
||||
def not_narrowed_str(x: str):
|
||||
if len(x):
|
||||
# No narrowing because `str` could be subclassed with a custom `__bool__`
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
def not_narrowed_list(x: list[int]):
|
||||
if len(x):
|
||||
# No narrowing because `list` could be subclassed with a custom `__bool__`
|
||||
reveal_type(x) # revealed: list[int]
|
||||
```
|
||||
|
||||
## Mixed unions (narrowable and non-narrowable)
|
||||
|
||||
When a union contains both narrowable and non-narrowable types, we narrow the narrowable parts while
|
||||
leaving the non-narrowable parts unchanged:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(x: Literal["foo", ""] | list[int]):
|
||||
if len(x):
|
||||
# `Literal[""]` is removed, `list[int]` is unchanged
|
||||
reveal_type(x) # revealed: Literal["foo"] | list[int]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[""] | list[int]
|
||||
```
|
||||
|
||||
## Narrowing away empty literals
|
||||
|
||||
This pattern is common when a prior truthiness check narrows a type, and then a conditional
|
||||
expression adds an empty literal back:
|
||||
|
||||
```py
|
||||
def _(lines: list[str]):
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
reveal_type(line) # revealed: str & ~AlwaysFalsy
|
||||
value = line if len(line) < 3 else ""
|
||||
reveal_type(value) # revealed: (str & ~AlwaysFalsy) | Literal[""]
|
||||
|
||||
if len(value):
|
||||
# `Literal[""]` is removed, `str & ~AlwaysFalsy` is unchanged
|
||||
reveal_type(value) # revealed: str & ~AlwaysFalsy
|
||||
# Accessing value[0] is safe here
|
||||
_ = value[0]
|
||||
```
|
||||
@@ -113,7 +113,7 @@ MyList: TypeAlias = list[T]
|
||||
ListOrSet: TypeAlias = list[T] | set[T]
|
||||
|
||||
reveal_type(MyList) # revealed: <class 'list[T]'>
|
||||
reveal_type(ListOrSet) # revealed: <types.UnionType special form 'list[T] | set[T]'>
|
||||
reveal_type(ListOrSet) # revealed: <types.UnionType special-form 'list[T] | set[T]'>
|
||||
|
||||
def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]):
|
||||
reveal_type(list_of_int) # revealed: list[int]
|
||||
@@ -293,7 +293,7 @@ def _(rec: RecursiveHomogeneousTuple):
|
||||
reveal_type(rec) # revealed: tuple[Divergent, ...]
|
||||
|
||||
ClassInfo: TypeAlias = type | UnionType | tuple["ClassInfo", ...]
|
||||
reveal_type(ClassInfo) # revealed: <types.UnionType special form 'type | UnionType | tuple[Divergent, ...]'>
|
||||
reveal_type(ClassInfo) # revealed: <types.UnionType special-form 'type | UnionType | tuple[Divergent, ...]'>
|
||||
|
||||
def my_isinstance(obj: object, classinfo: ClassInfo) -> bool:
|
||||
# TODO should be `type | UnionType | tuple[ClassInfo, ...]`
|
||||
|
||||
@@ -97,7 +97,7 @@ inside the module:
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = EllipsisType) -> None
|
||||
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ...) -> None
|
||||
|
||||
# For a stub module, we don't know that `__file__` is a string (at runtime it may be entirely
|
||||
# unset, but we follow typeshed here):
|
||||
|
||||
@@ -37,7 +37,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'C1'> | <class 'C1'>`
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
@@ -15,9 +15,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
|
||||
1 | from typing_extensions import deprecated
|
||||
2 |
|
||||
3 | @deprecated("use OtherClass")
|
||||
4 | def myfunc(): ...
|
||||
4 | def myfunc(x: int): ...
|
||||
5 |
|
||||
6 | myfunc() # error: [deprecated] "use OtherClass"
|
||||
6 | myfunc(1) # error: [deprecated] "use OtherClass"
|
||||
7 | from typing_extensions import deprecated
|
||||
8 |
|
||||
9 | @deprecated("use BetterClass")
|
||||
@@ -42,9 +42,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/deprecated.md
|
||||
warning[deprecated]: The function `myfunc` is deprecated
|
||||
--> src/mdtest_snippet.py:6:1
|
||||
|
|
||||
4 | def myfunc(): ...
|
||||
4 | def myfunc(x: int): ...
|
||||
5 |
|
||||
6 | myfunc() # error: [deprecated] "use OtherClass"
|
||||
6 | myfunc(1) # error: [deprecated] "use OtherClass"
|
||||
| ^^^^^^ use OtherClass
|
||||
7 | from typing_extensions import deprecated
|
||||
|
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - TypeVars with bounds that have the same name but are from different files
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## module.py
|
||||
|
||||
```
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from module import needs_a_foo
|
||||
2 |
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | def f[T: Foo](x: T) -> T:
|
||||
6 | needs_a_foo(x) # error: [invalid-argument-type]
|
||||
7 | return x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect
|
||||
--> src/main.py:6:17
|
||||
|
|
||||
5 | def f[T: Foo](x: T) -> T:
|
||||
6 | needs_a_foo(x) # error: [invalid-argument-type]
|
||||
| ^ Expected `Foo`, found `T@f`
|
||||
7 | return x
|
||||
|
|
||||
info: Function defined here
|
||||
--> src/module.py:3:5
|
||||
|
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
| ^^^^^^^^^^^ ------ Parameter declared here
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user