From 421f88bb327c8d4685a5e5cf4ca92533d30ef698 Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Wed, 17 Dec 2025 19:48:13 +0530 Subject: [PATCH] [`refurb`] Extend support for `Path.open` (`FURB101`, `FURB103`) (#21080) ## Summary This PR fixes https://github.com/astral-sh/ruff/issues/18409 ## Test Plan I have added tests in FURB103. --------- Signed-off-by: 11happy Signed-off-by: 11happy Co-authored-by: Brent Westbrook --- .../refurb/{FURB101.py => FURB101_0.py} | 3 +- .../test/fixtures/refurb/FURB101_1.py | 8 + .../refurb/{FURB103.py => FURB103_0.py} | 0 .../test/fixtures/refurb/FURB103_1.py | 26 +++ .../flake8_async/rules/blocking_open_call.rs | 2 +- .../src/rules/flake8_async/rules/mod.rs | 2 +- .../ruff_linter/src/rules/refurb/helpers.rs | 201 +++++++++++++----- crates/ruff_linter/src/rules/refurb/mod.rs | 8 +- .../src/rules/refurb/rules/read_whole_file.rs | 63 ++++-- .../rules/refurb/rules/write_whole_file.rs | 50 +++-- ..._refurb__tests__FURB101_FURB101_0.py.snap} | 36 ++-- ...__refurb__tests__FURB101_FURB101_1.py.snap | 39 ++++ ..._refurb__tests__FURB103_FURB103_0.py.snap} | 30 +-- ...__refurb__tests__FURB103_FURB103_1.py.snap | 157 ++++++++++++++ ...rb__tests__write_whole_file_python_39.snap | 24 +-- 15 files changed, 509 insertions(+), 140 deletions(-) rename crates/ruff_linter/resources/test/fixtures/refurb/{FURB101.py => FURB101_0.py} (98%) create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py rename crates/ruff_linter/resources/test/fixtures/refurb/{FURB103.py => FURB103_0.py} (100%) create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py rename crates/ruff_linter/src/rules/refurb/snapshots/{ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap => ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap} (91%) create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_1.py.snap rename crates/ruff_linter/src/rules/refurb/snapshots/{ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap => ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap} (96%) create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py similarity index 98% rename from crates/ruff_linter/resources/test/fixtures/refurb/FURB101.py rename to crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py index 77306cfe18..83f864a6e8 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_0.py @@ -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()) + diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py new file mode 100644 index 0000000000..e140135b70 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py @@ -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() \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py rename to crates/ruff_linter/resources/test/fixtures/refurb/FURB103_0.py diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py new file mode 100644 index 0000000000..f4b9cb1dec --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py @@ -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") \ No newline at end of file diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs index 0b1427c781..01bea3def4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs @@ -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; }; diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs index 3e12ea360c..ecdddaeda6 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs @@ -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; diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index a6871f0497..3b1d20e784 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -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> { 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> { + 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 = 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> { + 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. diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 9187853141..a98a770c93 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -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), )?; diff --git a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs index 2b43af89a8..187b04142e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs @@ -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 { - 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()) ) diff --git a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs index 8e4c7319ba..23a53e1f3a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs @@ -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 { - 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 diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap similarity index 91% rename from crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap rename to crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap index 3fea418d76..4d1b9b8edf 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_0.py.snap @@ -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")` diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_1.py.snap new file mode 100644 index 0000000000..61611fd967 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_1.py.snap @@ -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() diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap similarity index 96% rename from crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap rename to crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap index 81c35384b9..a22c150c6b 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap @@ -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: diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap new file mode 100644 index 0000000000..d30974009c --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap @@ -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") diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap index 7a5ae0c747..eaa3066929 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap @@ -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: