Skip to content

Commit 144373f

Browse files
chirizxcdanparizherntBre
authored
[flake8-use-pathlib] Fix PTH101, PTH104, PTH105, PTH121 fixes (#20143)
## Summary Fixes #20134 ## Test Plan `cargo nextest run flake8_use_pathlib` --------- Co-authored-by: Dan Parizher <danparizher@users.noreply.github.com> Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
1 parent 91995aa commit 144373f

File tree

9 files changed

+289
-51
lines changed

9 files changed

+289
-51
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,15 @@ def bar(x: int):
125125
os.makedirs(name="name", mode=0o777, exist_ok=False)
126126

127127
os.makedirs("name", unknown_kwarg=True)
128+
129+
# https://github.com/astral-sh/ruff/issues/20134
130+
os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131+
os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
132+
133+
# Only diagnostic
134+
os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135+
136+
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137+
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138+
139+
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)

crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
use ruff_python_ast::{self as ast, Expr, ExprCall};
1+
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
22
use ruff_python_semantic::{SemanticModel, analyze::typing};
33
use ruff_text_size::Ranged;
44

55
use crate::checkers::ast::Checker;
66
use crate::importer::ImportRequest;
77
use crate::{Applicability, Edit, Fix, Violation};
88

9-
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
9+
pub(crate) fn is_keyword_only_argument_non_default(arguments: &Arguments, name: &str) -> bool {
1010
arguments
1111
.find_keyword(name)
1212
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
@@ -24,10 +24,7 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
2424
/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support.
2525
/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for
2626
/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`.
27-
pub(crate) fn is_pure_path_subclass_with_preview(
28-
checker: &crate::checkers::ast::Checker,
29-
segments: &[&str],
30-
) -> bool {
27+
pub(crate) fn is_pure_path_subclass_with_preview(checker: &Checker, segments: &[&str]) -> bool {
3128
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
3229

3330
if is_core_pathlib {
@@ -193,7 +190,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
193190
}
194191

195192
pub(crate) fn has_unknown_keywords_or_starred_expr(
196-
arguments: &ast::Arguments,
193+
arguments: &Arguments,
197194
allowed: &[&str],
198195
) -> bool {
199196
if arguments.args.iter().any(Expr::is_starred_expr) {
@@ -207,11 +204,7 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
207204
}
208205

209206
/// Returns `true` if argument `name` is set to a non-default `None` value.
210-
pub(crate) fn is_argument_non_default(
211-
arguments: &ast::Arguments,
212-
name: &str,
213-
position: usize,
214-
) -> bool {
207+
pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, position: usize) -> bool {
215208
arguments
216209
.find_argument_value(name, position)
217210
.is_some_and(|expr| !expr.is_none_literal_expr())

crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_chmod.rs

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
use ruff_diagnostics::{Applicability, Edit, Fix};
2+
use ruff_macros::{ViolationMetadata, derive_message_formats};
3+
use ruff_python_ast::{ArgOrKeyword, ExprCall};
4+
use ruff_text_size::Ranged;
5+
16
use crate::checkers::ast::Checker;
7+
use crate::importer::ImportRequest;
28
use crate::preview::is_fix_os_chmod_enabled;
39
use crate::rules::flake8_use_pathlib::helpers::{
4-
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
10+
has_unknown_keywords_or_starred_expr, is_file_descriptor, is_keyword_only_argument_non_default,
11+
is_pathlib_path_call,
512
};
613
use crate::{FixAvailability, Violation};
7-
use ruff_macros::{ViolationMetadata, derive_message_formats};
8-
use ruff_python_ast::ExprCall;
914

1015
/// ## What it does
1116
/// Checks for uses of `os.chmod`.
@@ -73,22 +78,80 @@ pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
7378
// 0 1 2 3
7479
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
7580
// ```
76-
if call
77-
.arguments
78-
.find_argument_value("path", 0)
79-
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
81+
let path_arg = call.arguments.find_argument_value("path", 0);
82+
83+
if path_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
8084
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
8185
{
8286
return;
8387
}
8488

85-
check_os_pathlib_two_arg_calls(
86-
checker,
87-
call,
88-
"chmod",
89-
"path",
90-
"mode",
91-
is_fix_os_chmod_enabled(checker.settings()),
92-
OsChmod,
93-
);
89+
let range = call.range();
90+
let mut diagnostic = checker.report_diagnostic(OsChmod, call.func.range());
91+
92+
if !is_fix_os_chmod_enabled(checker.settings()) {
93+
return;
94+
}
95+
96+
if call.arguments.len() < 2 {
97+
return;
98+
}
99+
100+
if has_unknown_keywords_or_starred_expr(
101+
&call.arguments,
102+
&["path", "mode", "dir_fd", "follow_symlinks"],
103+
) {
104+
return;
105+
}
106+
107+
let (Some(path_arg), Some(_)) = (path_arg, call.arguments.find_argument_value("mode", 1))
108+
else {
109+
return;
110+
};
111+
112+
diagnostic.try_set_fix(|| {
113+
let (import_edit, binding) = checker.importer().get_or_import_symbol(
114+
&ImportRequest::import("pathlib", "Path"),
115+
call.start(),
116+
checker.semantic(),
117+
)?;
118+
119+
let locator = checker.locator();
120+
let path_code = locator.slice(path_arg.range());
121+
122+
let args = |arg: ArgOrKeyword| match arg {
123+
ArgOrKeyword::Arg(expr) if expr.range() != path_arg.range() => {
124+
Some(locator.slice(expr.range()))
125+
}
126+
ArgOrKeyword::Keyword(kw)
127+
if matches!(kw.arg.as_deref(), Some("mode" | "follow_symlinks")) =>
128+
{
129+
Some(locator.slice(kw.range()))
130+
}
131+
_ => None,
132+
};
133+
134+
let chmod_args = itertools::join(
135+
call.arguments.arguments_source_order().filter_map(args),
136+
", ",
137+
);
138+
139+
let replacement = if is_pathlib_path_call(checker, path_arg) {
140+
format!("{path_code}.chmod({chmod_args})")
141+
} else {
142+
format!("{binding}({path_code}).chmod({chmod_args})")
143+
};
144+
145+
let applicability = if checker.comment_ranges().intersects(range) {
146+
Applicability::Unsafe
147+
} else {
148+
Applicability::Safe
149+
};
150+
151+
Ok(Fix::applicable_edits(
152+
Edit::range_replacement(replacement, range),
153+
[import_edit],
154+
applicability,
155+
))
156+
});
94157
}

crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_path_samefile.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::checkers::ast::Checker;
22
use crate::preview::is_fix_os_path_samefile_enabled;
3-
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
3+
use crate::rules::flake8_use_pathlib::helpers::{
4+
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
5+
};
46
use crate::{FixAvailability, Violation};
57
use ruff_macros::{ViolationMetadata, derive_message_formats};
68
use ruff_python_ast::ExprCall;
@@ -65,13 +67,16 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
6567
return;
6668
}
6769

70+
let fix_enabled = is_fix_os_path_samefile_enabled(checker.settings())
71+
&& !has_unknown_keywords_or_starred_expr(&call.arguments, &["f1", "f2"]);
72+
6873
check_os_pathlib_two_arg_calls(
6974
checker,
7075
call,
7176
"samefile",
7277
"f1",
7378
"f2",
74-
is_fix_os_path_samefile_enabled(checker.settings()),
79+
fix_enabled,
7580
OsPathSamefile,
7681
);
7782
}

crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_rename.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::checkers::ast::Checker;
22
use crate::preview::is_fix_os_rename_enabled;
33
use crate::rules::flake8_use_pathlib::helpers::{
4-
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
4+
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
5+
is_keyword_only_argument_non_default,
56
};
67
use crate::{FixAvailability, Violation};
78
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -79,13 +80,11 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
7980
return;
8081
}
8182

82-
check_os_pathlib_two_arg_calls(
83-
checker,
84-
call,
85-
"rename",
86-
"src",
87-
"dst",
88-
is_fix_os_rename_enabled(checker.settings()),
89-
OsRename,
90-
);
83+
let fix_enabled = is_fix_os_rename_enabled(checker.settings())
84+
&& !has_unknown_keywords_or_starred_expr(
85+
&call.arguments,
86+
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
87+
);
88+
89+
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
9190
}

crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_replace.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::checkers::ast::Checker;
22
use crate::preview::is_fix_os_replace_enabled;
33
use crate::rules::flake8_use_pathlib::helpers::{
4-
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
4+
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
5+
is_keyword_only_argument_non_default,
56
};
67
use crate::{FixAvailability, Violation};
78
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -82,13 +83,19 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
8283
return;
8384
}
8485

86+
let fix_enabled = is_fix_os_replace_enabled(checker.settings())
87+
&& !has_unknown_keywords_or_starred_expr(
88+
&call.arguments,
89+
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
90+
);
91+
8592
check_os_pathlib_two_arg_calls(
8693
checker,
8794
call,
8895
"replace",
8996
"src",
9097
"dst",
91-
is_fix_os_replace_enabled(checker.settings()),
98+
fix_enabled,
9299
OsReplace,
93100
);
94101
}

crates/ruff_linter/src/rules/flake8_use_pathlib/rules/os_symlink.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,6 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
104104
return;
105105
};
106106

107-
let target_is_directory_arg = call.arguments.find_argument_value("target_is_directory", 2);
108-
109-
if let Some(expr) = &target_is_directory_arg {
110-
if expr.as_boolean_literal_expr().is_none() {
111-
return;
112-
}
113-
}
114-
115107
diagnostic.try_set_fix(|| {
116108
let (import_edit, binding) = checker.importer().get_or_import_symbol(
117109
&ImportRequest::import("pathlib", "Path"),
@@ -129,7 +121,9 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
129121
let src_code = locator.slice(src.range());
130122
let dst_code = locator.slice(dst.range());
131123

132-
let target_is_directory = target_is_directory_arg
124+
let target_is_directory = call
125+
.arguments
126+
.find_argument_value("target_is_directory", 2)
133127
.and_then(|expr| {
134128
let code = locator.slice(expr.range());
135129
expr.as_boolean_literal_expr()

crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,5 +500,72 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
500500
126 |
501501
127 | os.makedirs("name", unknown_kwarg=True)
502502
| ^^^^^^^^^^^
503+
128 |
504+
129 | # https://github.com/astral-sh/ruff/issues/20134
503505
|
504506
help: Replace with `Path(...).mkdir(parents=True)`
507+
508+
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
509+
--> full_name.py:130:1
510+
|
511+
129 | # https://github.com/astral-sh/ruff/issues/20134
512+
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
513+
| ^^^^^^^^
514+
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
515+
|
516+
help: Replace with `Path(...).chmod(...)`
517+
518+
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
519+
--> full_name.py:131:1
520+
|
521+
129 | # https://github.com/astral-sh/ruff/issues/20134
522+
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
523+
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
524+
| ^^^^^^^^
525+
132 |
526+
133 | # Only diagnostic
527+
|
528+
help: Replace with `Path(...).chmod(...)`
529+
530+
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
531+
--> full_name.py:134:1
532+
|
533+
133 | # Only diagnostic
534+
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
535+
| ^^^^^^^^
536+
135 |
537+
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
538+
|
539+
help: Replace with `Path(...).chmod(...)`
540+
541+
PTH104 `os.rename()` should be replaced by `Path.rename()`
542+
--> full_name.py:136:1
543+
|
544+
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
545+
135 |
546+
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
547+
| ^^^^^^^^^
548+
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
549+
|
550+
help: Replace with `Path(...).rename(...)`
551+
552+
PTH105 `os.replace()` should be replaced by `Path.replace()`
553+
--> full_name.py:137:1
554+
|
555+
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
556+
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
557+
| ^^^^^^^^^^
558+
138 |
559+
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
560+
|
561+
help: Replace with `Path(...).replace(...)`
562+
563+
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
564+
--> full_name.py:139:1
565+
|
566+
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
567+
138 |
568+
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
569+
| ^^^^^^^^^^^^^^^^
570+
|
571+
help: Replace with `Path(...).samefile()`

0 commit comments

Comments
 (0)