[`flake8-use-pathlib`] Add fix for `PTH123` (#20169)

## Summary
Part of https://github.com/astral-sh/ruff/issues/2331

## Test Plan
`cargo nextest run flake8_use_pathlib`

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
chiri 2025-09-17 22:47:29 +03:00 committed by GitHub
parent 05622ae757
commit bfb0902446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 783 additions and 102 deletions

View File

@ -0,0 +1,24 @@
import builtins
from pathlib import Path
_file = "file.txt"
_x = ("r+", -1)
r_plus = "r+"
builtins.open(file=_file)
open(_file, "r+ ", - 1)
open(mode="wb", file=_file)
open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
open(_file, "r+", - 1, None, None, None, True, None)
open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
open(_file, f" {r_plus} ", - 1)
open(buffering=- 1, file=_file, encoding= "utf-8")
# Only diagnostic
open()
open(_file, *_x)
open(_file, "r+", unknown=True)
open(_file, "r+", closefd=False)
open(_file, "r+", None, None, None, None, None, None, None)

View File

@ -1051,7 +1051,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::OsStat,
Rule::OsPathJoin,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
Rule::Glob,
Rule::OsListdir,
@ -1135,6 +1134,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsSymlink) {
flake8_use_pathlib::rules::os_symlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::BuiltinOpen) {
flake8_use_pathlib::rules::builtin_open(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,

View File

@ -944,7 +944,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::BuiltinOpen),
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),
(Flake8UsePathlib, "201") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory),
(Flake8UsePathlib, "202") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetsize),

View File

@ -223,3 +223,8 @@ pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled(
pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20169
pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@ -205,3 +205,14 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
None => true,
})
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
pub(crate) fn is_argument_non_default(
arguments: &ast::Arguments,
name: &str,
position: usize,
) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}

View File

@ -59,6 +59,7 @@ mod tests {
#[test_case(Rule::PyPath, Path::new("py_path_1.py"))]
#[test_case(Rule::PyPath, Path::new("py_path_2.py"))]
#[test_case(Rule::BuiltinOpen, Path::new("PTH123.py"))]
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
@ -123,6 +124,7 @@ mod tests {
Ok(())
}
#[test_case(Rule::BuiltinOpen, Path::new("PTH123.py"))]
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]

View File

@ -0,0 +1,191 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ArgOrKeyword, Expr, ExprBooleanLiteral, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_builtin_open_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_argument_non_default, is_file_descriptor,
is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of the `open()` builtin.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation. When possible,
/// using `Path` object methods such as `Path.open()` can improve readability
/// over the `open` builtin.
///
/// ## Examples
/// ```python
/// with open("f1.py", "wb") as fp:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// with Path("f1.py").open("wb") as fp:
/// ...
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than working directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct BuiltinOpen;
impl Violation for BuiltinOpen {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`open()` should be replaced by `Path.open()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.open()`".to_string())
}
}
// PTH123
pub(crate) fn builtin_open(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `closefd` and `opener` are not supported by pathlib, so check if they
// are set to non-default values.
// https://github.com/astral-sh/ruff/issues/7620
// Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open):
// ```text
// builtins.open(
// file, 0
// mode='r', 1
// buffering=-1, 2
// encoding=None, 3
// errors=None, 4
// newline=None, 5
// closefd=True, 6 <= not supported
// opener=None 7 <= not supported
// )
// ```
// For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open):
// ```text
// Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
// ```
let file_arg = call.arguments.find_argument_value("file", 0);
if call
.arguments
.find_argument_value("closefd", 6)
.is_some_and(|expr| {
!matches!(
expr,
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
)
})
|| is_argument_non_default(&call.arguments, "opener", 7)
|| file_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
}
if !matches!(segments, ["" | "builtins", "open"]) {
return;
}
let mut diagnostic = checker.report_diagnostic(BuiltinOpen, call.func.range());
if !is_fix_builtin_open_enabled(checker.settings()) {
return;
}
let Some(file) = file_arg else {
return;
};
if has_unknown_keywords_or_starred_expr(
&call.arguments,
&[
"file",
"mode",
"buffering",
"encoding",
"errors",
"newline",
"closefd",
"opener",
],
) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let locator = checker.locator();
let file_code = locator.slice(file.range());
let args = |i: usize, arg: ArgOrKeyword| match arg {
ArgOrKeyword::Arg(expr) => {
if expr.range() == file.range() || i == 6 || i == 7 {
None
} else {
Some(locator.slice(expr.range()))
}
}
ArgOrKeyword::Keyword(kw) => match kw.arg.as_deref() {
Some("mode" | "buffering" | "encoding" | "errors" | "newline") => {
Some(locator.slice(kw))
}
_ => None,
},
};
let open_args = itertools::join(
call.arguments
.arguments_source_order()
.enumerate()
.filter_map(|(i, arg)| args(i, arg)),
", ",
);
let replacement = if is_pathlib_path_call(checker, file) {
format!("{file_code}.open({open_args})")
} else {
format!("{binding}({file_code}).open({open_args})")
};
let range = call.range();
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}

View File

@ -1,3 +1,4 @@
pub(crate) use builtin_open::*;
pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_chmod::*;
@ -29,6 +30,7 @@ pub(crate) use os_unlink::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;
mod builtin_open;
mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_chmod;

View File

@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@ -7,7 +7,7 @@ use crate::rules::flake8_use_pathlib::helpers::{
};
use crate::rules::flake8_use_pathlib::{
rules::Glob,
violations::{BuiltinOpen, Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath},
violations::{Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath},
};
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
@ -60,42 +60,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
),
// PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH123
["" | "builtins", "open"] => {
// `closefd` and `opener` are not supported by pathlib, so check if they
// are set to non-default values.
// https://github.com/astral-sh/ruff/issues/7620
// Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open):
// ```text
// 0 1 2 3 4 5
// open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None,
// 6 7
// closefd=True, opener=None)
// ^^^^ ^^^^
// ```
// For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open):
// ```text
// Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
// ```
if call
.arguments
.find_argument_value("closefd", 6)
.is_some_and(|expr| {
!matches!(
expr,
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
)
})
|| is_argument_non_default(&call.arguments, "opener", 7)
|| call
.arguments
.find_argument_value("file", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
}
checker.report_diagnostic_if_enabled(BuiltinOpen, range)
}
// PTH124
["py", "path", "local"] => checker.report_diagnostic_if_enabled(PyPath, range),
// PTH207
@ -151,10 +115,3 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
_ => return,
};
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}

View File

@ -0,0 +1,143 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:8:1
|
6 | r_plus = "r+"
7 |
8 | builtins.open(file=_file)
| ^^^^^^^^^^^^^
9 |
10 | open(_file, "r+ ", - 1)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:10:1
|
8 | builtins.open(file=_file)
9 |
10 | open(_file, "r+ ", - 1)
| ^^^^
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:11:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
| ^^^^
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:12:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
| ^^^^
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:13:1
|
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
| ^^^^
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:14:1
|
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
| ^^^^
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:15:1
|
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
| ^^^^
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:16:1
|
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
| ^^^^
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:17:1
|
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
| ^^^^
18 |
19 | # Only diagnostic
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:20:1
|
19 | # Only diagnostic
20 | open()
| ^^^^
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:21:1
|
19 | # Only diagnostic
20 | open()
21 | open(_file, *_x)
| ^^^^
22 | open(_file, "r+", unknown=True)
23 | open(_file, "r+", closefd=False)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:22:1
|
20 | open()
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
| ^^^^
23 | open(_file, "r+", closefd=False)
24 | open(_file, "r+", None, None, None, None, None, None, None)
|
help: Replace with `Path.open()`

View File

@ -305,6 +305,7 @@ PTH123 `open()` should be replaced by `Path.open()`
33 | fp.read()
34 | open(p).close()
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:34:1
@ -316,6 +317,7 @@ PTH123 `open()` should be replaced by `Path.open()`
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
|
help: Replace with `Path.open()`
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:35:1
@ -360,6 +362,7 @@ PTH123 `open()` should be replaced by `Path.open()`
47 | open(p, 'r', - 1, None, None, None, True, None)
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:47:1
@ -370,6 +373,7 @@ PTH123 `open()` should be replaced by `Path.open()`
| ^^^^
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:65:1
@ -381,6 +385,7 @@ PTH123 `open()` should be replaced by `Path.open()`
66 | byte_str = b"bar"
67 | open(byte_str)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:67:1
@ -392,6 +397,7 @@ PTH123 `open()` should be replaced by `Path.open()`
68 |
69 | def bytes_str_func() -> bytes:
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:71:1
@ -403,6 +409,7 @@ PTH123 `open()` should be replaced by `Path.open()`
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|
help: Replace with `Path.open()`
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:108:1

View File

@ -305,6 +305,7 @@ PTH123 `open()` should be replaced by `Path.open()`
35 | fp.read()
36 | open(p).close()
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:36:1
@ -314,6 +315,7 @@ PTH123 `open()` should be replaced by `Path.open()`
36 | open(p).close()
| ^^^^
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:43:10
@ -323,6 +325,7 @@ PTH123 `open()` should be replaced by `Path.open()`
43 | with open(p) as _: ... # Error
| ^^^^
|
help: Replace with `Path.open()`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_from.py:53:1

View File

@ -0,0 +1,215 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:8:1
|
6 | r_plus = "r+"
7 |
8 | builtins.open(file=_file)
| ^^^^^^^^^^^^^
9 |
10 | open(_file, "r+ ", - 1)
|
help: Replace with `Path.open()`
5 | _x = ("r+", -1)
6 | r_plus = "r+"
7 |
- builtins.open(file=_file)
8 + Path(_file).open()
9 |
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:10:1
|
8 | builtins.open(file=_file)
9 |
10 | open(_file, "r+ ", - 1)
| ^^^^
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
|
help: Replace with `Path.open()`
7 |
8 | builtins.open(file=_file)
9 |
- open(_file, "r+ ", - 1)
10 + Path(_file).open("r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:11:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
| ^^^^
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
|
help: Replace with `Path.open()`
8 | builtins.open(file=_file)
9 |
10 | open(_file, "r+ ", - 1)
- open(mode="wb", file=_file)
11 + Path(_file).open(mode="wb")
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:12:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
| ^^^^
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
|
help: Replace with `Path.open()`
9 |
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
- open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
12 + Path(_file).open(mode="r+", buffering=-1, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:13:1
|
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
| ^^^^
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
|
help: Replace with `Path.open()`
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
- open(_file, "r+", - 1, None, None, None, True, None)
13 + Path(_file).open("r+", - 1, None, None, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:14:1
|
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
| ^^^^
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
|
help: Replace with `Path.open()`
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
- open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
14 + Path(_file).open("r+", -1, None, None, None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:15:1
|
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
| ^^^^
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
- open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
15 + Path(_file).open(mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
18 |
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:16:1
|
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
| ^^^^
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
- open(_file, f" {r_plus} ", - 1)
16 + Path(_file).open(f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
18 |
19 | # Only diagnostic
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:17:1
|
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
| ^^^^
18 |
19 | # Only diagnostic
|
help: Replace with `Path.open()`
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
- open(buffering=- 1, file=_file, encoding= "utf-8")
17 + Path(_file).open(buffering=- 1, encoding= "utf-8")
18 |
19 | # Only diagnostic
20 | open()
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:20:1
|
19 | # Only diagnostic
20 | open()
| ^^^^
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:21:1
|
19 | # Only diagnostic
20 | open()
21 | open(_file, *_x)
| ^^^^
22 | open(_file, "r+", unknown=True)
23 | open(_file, "r+", closefd=False)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:22:1
|
20 | open()
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
| ^^^^
23 | open(_file, "r+", closefd=False)
24 | open(_file, "r+", None, None, None, None, None, None, None)
|
help: Replace with `Path.open()`

View File

@ -520,7 +520,7 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an
33 | fp.read()
|
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:32:6
|
30 | os.path.samefile(p)
@ -530,8 +530,24 @@ PTH123 `open()` should be replaced by `Path.open()`
33 | fp.read()
34 | open(p).close()
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
30 | os.path.dirname(p)
31 | os.path.samefile(p)
32 | os.path.splitext(p)
- with open(p) as fp:
33 + with pathlib.Path(p).open() as fp:
34 | fp.read()
35 | open(p).close()
36 | os.getcwdb(p)
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:34:1
|
32 | with open(p) as fp:
@ -541,6 +557,22 @@ PTH123 `open()` should be replaced by `Path.open()`
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
32 | os.path.splitext(p)
33 | with open(p) as fp:
34 | fp.read()
- open(p).close()
35 + pathlib.Path(p).open().close()
36 | os.getcwdb(p)
37 | os.path.join(p, *q)
38 | os.sep.join(p, *q)
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:35:1
@ -575,7 +607,7 @@ PTH118 `os.sep.join()` should be replaced by `Path.joinpath()`
39 | # https://github.com/astral-sh/ruff/issues/7620
|
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:46:1
|
44 | open(p, closefd=False)
@ -585,8 +617,24 @@ PTH123 `open()` should be replaced by `Path.open()`
47 | open(p, 'r', - 1, None, None, None, True, None)
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
44 |
45 | open(p, closefd=False)
46 | open(p, opener=opener)
- open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
47 + pathlib.Path(p).open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
48 | open(p, 'r', - 1, None, None, None, True, None)
49 | open(p, 'r', - 1, None, None, None, False, opener)
50 |
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:47:1
|
45 | open(p, opener=opener)
@ -595,8 +643,24 @@ PTH123 `open()` should be replaced by `Path.open()`
| ^^^^
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
45 | open(p, closefd=False)
46 | open(p, opener=opener)
47 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
- open(p, 'r', - 1, None, None, None, True, None)
48 + pathlib.Path(p).open('r', - 1, None, None, None)
49 | open(p, 'r', - 1, None, None, None, False, opener)
50 |
51 | # Cannot be upgraded `pathlib.Open` does not support fds
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:65:1
|
63 | open(f())
@ -606,8 +670,24 @@ PTH123 `open()` should be replaced by `Path.open()`
66 | byte_str = b"bar"
67 | open(byte_str)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
63 | return 1
64 | open(f())
65 |
- open(b"foo")
66 + pathlib.Path(b"foo").open()
67 | byte_str = b"bar"
68 | open(byte_str)
69 |
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:67:1
|
65 | open(b"foo")
@ -617,8 +697,24 @@ PTH123 `open()` should be replaced by `Path.open()`
68 |
69 | def bytes_str_func() -> bytes:
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
65 |
66 | open(b"foo")
67 | byte_str = b"bar"
- open(byte_str)
68 + pathlib.Path(byte_str).open()
69 |
70 | def bytes_str_func() -> bytes:
71 | return b"foo"
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> full_name.py:71:1
|
69 | def bytes_str_func() -> bytes:
@ -628,6 +724,22 @@ PTH123 `open()` should be replaced by `Path.open()`
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
69 |
70 | def bytes_str_func() -> bytes:
71 | return b"foo"
- open(bytes_str_func())
72 + pathlib.Path(bytes_str_func()).open()
73 |
74 | # https://github.com/astral-sh/ruff/issues/17693
75 | os.stat(1)
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:108:1

View File

@ -535,7 +535,7 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an
35 | fp.read()
|
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> import_from.py:34:6
|
32 | samefile(p)
@ -545,8 +545,25 @@ PTH123 `open()` should be replaced by `Path.open()`
35 | fp.read()
36 | open(p).close()
|
help: Replace with `Path.open()`
2 | from os import remove, unlink, getcwd, readlink, stat
3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 + import pathlib
6 |
7 | p = "/foo"
8 | q = "bar"
--------------------------------------------------------------------------------
32 | dirname(p)
33 | samefile(p)
34 | splitext(p)
- with open(p) as fp:
35 + with pathlib.Path(p).open() as fp:
36 | fp.read()
37 | open(p).close()
38 |
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> import_from.py:36:1
|
34 | with open(p) as fp:
@ -554,8 +571,25 @@ PTH123 `open()` should be replaced by `Path.open()`
36 | open(p).close()
| ^^^^
|
help: Replace with `Path.open()`
2 | from os import remove, unlink, getcwd, readlink, stat
3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 + import pathlib
6 |
7 | p = "/foo"
8 | q = "bar"
--------------------------------------------------------------------------------
34 | splitext(p)
35 | with open(p) as fp:
36 | fp.read()
- open(p).close()
37 + pathlib.Path(p).open().close()
38 |
39 |
40 | # https://github.com/astral-sh/ruff/issues/15442
PTH123 `open()` should be replaced by `Path.open()`
PTH123 [*] `open()` should be replaced by `Path.open()`
--> import_from.py:43:10
|
41 | from builtins import open
@ -563,6 +597,23 @@ PTH123 `open()` should be replaced by `Path.open()`
43 | with open(p) as _: ... # Error
| ^^^^
|
help: Replace with `Path.open()`
2 | from os import remove, unlink, getcwd, readlink, stat
3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 + import pathlib
6 |
7 | p = "/foo"
8 | q = "bar"
--------------------------------------------------------------------------------
41 | def _():
42 | from builtins import open
43 |
- with open(p) as _: ... # Error
44 + with pathlib.Path(p).open() as _: ... # Error
45 |
46 |
47 | def _():
PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
--> import_from.py:53:1

View File

@ -174,50 +174,6 @@ impl Violation for OsPathSplitext {
}
}
/// ## What it does
/// Checks for uses of the `open()` builtin.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation. When possible,
/// using `Path` object methods such as `Path.open()` can improve readability
/// over the `open` builtin.
///
/// ## Examples
/// ```python
/// with open("f1.py", "wb") as fp:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// with Path("f1.py").open("wb") as fp:
/// ...
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than working directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct BuiltinOpen;
impl Violation for BuiltinOpen {
#[derive_message_formats]
fn message(&self) -> String {
"`open()` should be replaced by `Path.open()`".to_string()
}
}
/// ## What it does
/// Checks for uses of the `py.path` library.
///