This commit is contained in:
Bhuminjay Soni 2025-12-16 16:37:01 -05:00 committed by GitHub
commit 2aa9f82777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 403 additions and 108 deletions

View 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")

View File

@ -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`. /// 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 { let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
return false; return false;
}; };

View File

@ -18,7 +18,7 @@ mod async_zero_sleep;
mod blocking_http_call; mod blocking_http_call;
mod blocking_http_call_httpx; mod blocking_http_call_httpx;
mod blocking_input; mod blocking_input;
mod blocking_open_call; pub(crate) mod blocking_open_call;
mod blocking_path_methods; mod blocking_path_methods;
mod blocking_process_invocation; mod blocking_process_invocation;
mod blocking_sleep; mod blocking_sleep;

View File

@ -3,10 +3,11 @@ use std::borrow::Cow;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range}; use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range};
use ruff_python_codegen::Generator; 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 ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::flake8_async::rules::blocking_open_call::is_open_call_from_pathlib;
use crate::{Applicability, Edit, Fix}; use crate::{Applicability, Edit, Fix};
/// Format a code snippet to call `name.method()`. /// Format a code snippet to call `name.method()`.
@ -119,14 +120,13 @@ impl OpenMode {
pub(super) struct FileOpen<'a> { pub(super) struct FileOpen<'a> {
/// With item where the open happens, we use it for the reporting range. /// With item where the open happens, we use it for the reporting range.
pub(super) item: &'a ast::WithItem, 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. /// The file open mode.
pub(super) mode: OpenMode, pub(super) mode: OpenMode,
/// The file open keywords. /// The file open keywords.
pub(super) keywords: Vec<&'a ast::Keyword>, pub(super) keywords: Vec<&'a ast::Keyword>,
/// We only check `open` operations whose file handles are used exactly once. /// We only check `open` operations whose file handles are used exactly once.
pub(super) reference: &'a ResolvedReference, pub(super) reference: &'a ResolvedReference,
pub(super) argument: OpenArgument<'a>,
} }
impl FileOpen<'_> { 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. /// Find and return all `open` operations in the given `with` statement.
pub(super) fn find_file_opens<'a>( pub(super) fn find_file_opens<'a>(
with: &'a ast::StmtWith, with: &'a ast::StmtWith,
@ -146,10 +185,65 @@ pub(super) fn find_file_opens<'a>(
) -> Vec<FileOpen<'a>> { ) -> Vec<FileOpen<'a>> {
with.items with.items
.iter() .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() .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. /// Find `open` operation in the given `with` item.
fn find_file_open<'a>( fn find_file_open<'a>(
item: &'a ast::WithItem, item: &'a ast::WithItem,
@ -165,8 +259,6 @@ fn find_file_open<'a>(
.. ..
} = item.context_expr.as_call_expr()?; } = 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")`, // 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 // it could be a match; but in all other cases, the call _could_ contain unsupported keyword
// arguments, like `buffering`. // 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 (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
let mode = kw_mode.unwrap_or(pos_mode); let mode = kw_mode.unwrap_or(pos_mode);
resolve_file_open(
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 {
item, item,
filename, with,
semantic,
read_mode,
mode, mode,
keywords, 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. /// Match positional arguments. Return expression for the file name and open mode.

View File

@ -46,7 +46,8 @@ mod tests {
#[test_case(Rule::MetaClassABCMeta, Path::new("FURB180.py"))] #[test_case(Rule::MetaClassABCMeta, Path::new("FURB180.py"))]
#[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))] #[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))]
#[test_case(Rule::ListReverseCopy, Path::new("FURB187.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::FStringNumberFormat, Path::new("FURB116.py"))]
#[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))] #[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))]
#[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))] #[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))]
@ -65,7 +66,7 @@ mod tests {
#[test] #[test]
fn write_whole_file_python_39() -> Result<()> { fn write_whole_file_python_39() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(
Path::new("refurb/FURB103.py"), Path::new("refurb/FURB103_0.py"),
&settings::LinterSettings::for_rule(Rule::WriteWholeFile) &settings::LinterSettings::for_rule(Rule::WriteWholeFile)
.with_target_version(PythonVersion::PY39), .with_target_version(PythonVersion::PY39),
)?; )?;

View File

@ -10,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet; use crate::fix::snippet::SourceCodeSnippet;
use crate::importer::ImportRequest; 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}; use crate::{FixAvailability, Violation};
/// ## What it does /// ## What it does
@ -114,12 +114,11 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
.position(|open| open.is_ref(read_from)) .position(|open| open.is_ref(read_from))
{ {
let open = self.candidates.remove(open); let open = self.candidates.remove(open);
let filename_display = open.argument.display(self.checker.source());
let suggestion = make_suggestion(&open, self.checker.generator()); let suggestion = make_suggestion(&open, self.checker.generator());
let mut diagnostic = self.checker.report_diagnostic( let mut diagnostic = self.checker.report_diagnostic(
ReadWholeFile { ReadWholeFile {
filename: SourceCodeSnippet::from_str( filename: SourceCodeSnippet::from_str(filename_display),
&self.checker.generator().expr(open.filename),
),
suggestion: SourceCodeSnippet::from_str(&suggestion), suggestion: SourceCodeSnippet::from_str(&suggestion),
}, },
open.item.range(), open.item.range(),
@ -185,10 +184,11 @@ fn generate_fix(
if with_stmt.items.len() != 1 { if with_stmt.items.len() != 1 {
return None; return None;
} }
let OpenArgument::Builtin { filename } = open.argument else {
return None;
};
let locator = checker.locator(); let locator = checker.locator();
let filename_code = locator.slice(filename.range());
let filename_code = locator.slice(open.filename.range());
let (import_edit, binding) = checker let (import_edit, binding) = checker
.importer() .importer()

View File

@ -9,7 +9,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet; use crate::fix::snippet::SourceCodeSnippet;
use crate::importer::ImportRequest; 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}; use crate::{FixAvailability, Locator, Violation};
/// ## What it does /// ## 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) /// - [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text)
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.3.6")] #[violation_metadata(preview_since = "v0.3.6")]
pub(crate) struct WriteWholeFile { pub(crate) struct WriteWholeFile<'a> {
filename: SourceCodeSnippet, filename: SourceCodeSnippet,
suggestion: SourceCodeSnippet, suggestion: SourceCodeSnippet,
argument: OpenArgument<'a>,
} }
impl Violation for WriteWholeFile { impl Violation for WriteWholeFile<'_> {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let filename = self.filename.truncated_display(); let filename = self.filename.truncated_display();
let suggestion = self.suggestion.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> { fn fix_title(&self) -> Option<String> {
Some(format!( let filename = self.filename.truncated_display();
"Replace with `Path({}).{}`", let suggestion = self.suggestion.truncated_display();
self.filename.truncated_display(),
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)) .position(|open| open.is_ref(write_to))
{ {
let open = self.candidates.remove(open); let open = self.candidates.remove(open);
if self.loop_counter == 0 { if self.loop_counter == 0 {
let filename_display = open.argument.display(self.checker.source());
let suggestion = make_suggestion(&open, content, self.checker.locator()); let suggestion = make_suggestion(&open, content, self.checker.locator());
let mut diagnostic = self.checker.report_diagnostic( let mut diagnostic = self.checker.report_diagnostic(
WriteWholeFile { WriteWholeFile {
filename: SourceCodeSnippet::from_str( filename: SourceCodeSnippet::from_str(filename_display),
&self.checker.generator().expr(open.filename),
),
suggestion: SourceCodeSnippet::from_str(&suggestion), suggestion: SourceCodeSnippet::from_str(&suggestion),
argument: open.argument,
}, },
open.item.range(), open.item.range(),
); );
@ -198,7 +211,6 @@ fn generate_fix(
} }
let locator = checker.locator(); let locator = checker.locator();
let filename_code = locator.slice(open.filename.range());
let (import_edit, binding) = checker let (import_edit, binding) = checker
.importer() .importer()
@ -209,7 +221,15 @@ fn generate_fix(
) )
.ok()?; .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()) { let applicability = if checker.comment_ranges().intersects(with_stmt.range()) {
Applicability::Unsafe Applicability::Unsafe

View File

@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/refurb/mod.rs source: crates/ruff_linter/src/rules/refurb/mod.rs
--- ---
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")` 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 11 | # FURB103
12 | with open("file.txt", "w") as f: 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: 16 | with open("file.txt", "wb") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)` 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 15 | # FURB103
16 | with open("file.txt", "wb") as f: 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: 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 [*] `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 19 | # FURB103
20 | with open("file.txt", mode="wb") as f: 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: 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 [*] `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 23 | # FURB103
24 | with open("file.txt", "w", encoding="utf8") as f: 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: 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 [*] `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 27 | # FURB103
28 | with open("file.txt", "w", errors="ignore") as f: 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: 32 | with open("file.txt", mode="w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)` 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 31 | # FURB103
32 | with open("file.txt", mode="w") as f: 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: 36 | with open(foo(), "wb") as f:
FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())`
--> FURB103.py:36:6 --> FURB103_0.py:36:6
| |
35 | # FURB103 35 | # FURB103
36 | with open(foo(), "wb") as f: 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())` help: Replace with `Path(foo()).write_bytes(bar())`
FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` 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 43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: 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)` help: Replace with `Path("a.txt").write_text(x)`
FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` 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 43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: 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)` 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 `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 48 | # FURB103
49 | with foo() as a, open("file.txt", "w") as b, foo() as c: 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)))` 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 [*] `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 57 | # FURB103
58 | with open("file.txt", "w", newline="\r\n") as f: 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 62 | import builtins
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` 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 65 | # FURB103
66 | with builtins.open("file.txt", "w", newline="\r\n") as f: 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 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 [*] `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 73 | # FURB103
74 | with o("file.txt", "w", newline="\r\n") as f: 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 | 78 |
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....` FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103.py:154:6 --> FURB103_0.py:154:6
| |
152 | data = {"price": 100} 152 | data = {"price": 100}
153 | 153 |
@ -284,7 +284,7 @@ help: Replace with `Path("test.json")....`
158 | with open("tmp_path/pyproject.toml", "w") as f: 158 | with open("tmp_path/pyproject.toml", "w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....` 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 157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f: 158 | with open("tmp_path/pyproject.toml", "w") as f:

View File

@ -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")

View File

@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/refurb/mod.rs source: crates/ruff_linter/src/rules/refurb/mod.rs
--- ---
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")` 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 11 | # FURB103
12 | with open("file.txt", "w") as f: 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: 16 | with open("file.txt", "wb") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)` 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 15 | # FURB103
16 | with open("file.txt", "wb") as f: 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: 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 [*] `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 19 | # FURB103
20 | with open("file.txt", mode="wb") as f: 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: 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 [*] `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 23 | # FURB103
24 | with open("file.txt", "w", encoding="utf8") as f: 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: 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 [*] `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 27 | # FURB103
28 | with open("file.txt", "w", errors="ignore") as f: 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: 32 | with open("file.txt", mode="w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)` 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 31 | # FURB103
32 | with open("file.txt", mode="w") as f: 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: 36 | with open(foo(), "wb") as f:
FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())`
--> FURB103.py:36:6 --> FURB103_0.py:36:6
| |
35 | # FURB103 35 | # FURB103
36 | with open(foo(), "wb") as f: 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())` help: Replace with `Path(foo()).write_bytes(bar())`
FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` 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 43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: 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)` help: Replace with `Path("a.txt").write_text(x)`
FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` 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 43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: 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)` 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 `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 48 | # FURB103
49 | with foo() as a, open("file.txt", "w") as b, foo() as c: 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)))` help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....` FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103.py:154:6 --> FURB103_0.py:154:6
| |
152 | data = {"price": 100} 152 | data = {"price": 100}
153 | 153 |
@ -214,7 +214,7 @@ help: Replace with `Path("test.json")....`
158 | with open("tmp_path/pyproject.toml", "w") as f: 158 | with open("tmp_path/pyproject.toml", "w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....` 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 157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f: 158 | with open("tmp_path/pyproject.toml", "w") as f: