Skip to content

Commit 0793c08

Browse files
LaBatata101Glyphack
authored andcommitted
[flake8-use-pathlib] PTH* suppress diagnostic for all os.* functions that have the dir_fd parameter (astral-sh#17968)
<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> Fixes astral-sh#17776. This PR also handles all other `PTH*` rules that don't support file descriptors. ## Test Plan <!-- How was it tested? --> Update existing tests.
1 parent 9d86021 commit 0793c08

File tree

4 files changed

+166
-29
lines changed

4 files changed

+166
-29
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@
99
glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))
1010
list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")))
1111
search("*.png")
12+
13+
# if `dir_fd` is set, suppress the diagnostic
14+
glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1)
15+
list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1))

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,20 @@ def bar(x: int):
8787
os.rename("src", "dst", src_dir_fd=3, dst_dir_fd=4)
8888
os.rename("src", "dst", src_dir_fd=3)
8989
os.rename("src", "dst", dst_dir_fd=4)
90+
91+
# if `dir_fd` is set, suppress the diagnostic
92+
os.readlink(p, dir_fd=1)
93+
os.stat(p, dir_fd=2)
94+
os.unlink(p, dir_fd=3)
95+
os.remove(p, dir_fd=4)
96+
os.rmdir(p, dir_fd=5)
97+
os.mkdir(p, dir_fd=6)
98+
os.chmod(p, dir_fd=7)
99+
# `chmod` can also receive a file descriptor in the first argument
100+
os.chmod(8)
101+
os.chmod(x)
102+
103+
# if `src_dir_fd` or `dst_dir_fd` are set, suppress the diagnostic
104+
os.replace("src", "dst", src_dir_fd=1, dst_dir_fd=2)
105+
os.replace("src", "dst", src_dir_fd=1)
106+
os.replace("src", "dst", dst_dir_fd=2)

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

Lines changed: 143 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,41 +26,109 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
2626
// PTH100
2727
["os", "path", "abspath"] => OsPathAbspath.into(),
2828
// PTH101
29-
["os", "chmod"] => OsChmod.into(),
29+
["os", "chmod"] => {
30+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
31+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
32+
// ```text
33+
// 0 1 2 3
34+
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
35+
// ```
36+
if call
37+
.arguments
38+
.find_argument_value("path", 0)
39+
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
40+
|| is_argument_non_default(&call.arguments, "dir_fd", 2)
41+
{
42+
return;
43+
}
44+
OsChmod.into()
45+
}
3046
// PTH102
3147
["os", "makedirs"] => OsMakedirs.into(),
3248
// PTH103
33-
["os", "mkdir"] => OsMkdir.into(),
49+
["os", "mkdir"] => {
50+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
51+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.mkdir)
52+
// ```text
53+
// 0 1 2
54+
// os.mkdir(path, mode=0o777, *, dir_fd=None)
55+
// ```
56+
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
57+
return;
58+
}
59+
OsMkdir.into()
60+
}
3461
// PTH104
3562
["os", "rename"] => {
3663
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
37-
// are set to non-default values.
64+
// set to non-default values.
3865
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
3966
// ```text
4067
// 0 1 2 3
4168
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
4269
// ```
43-
if call
44-
.arguments
45-
.find_argument_value("src_dir_fd", 2)
46-
.is_some_and(|expr| !expr.is_none_literal_expr())
47-
|| call
48-
.arguments
49-
.find_argument_value("dst_dir_fd", 3)
50-
.is_some_and(|expr| !expr.is_none_literal_expr())
70+
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
71+
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
5172
{
5273
return;
5374
}
5475
OsRename.into()
5576
}
5677
// PTH105
57-
["os", "replace"] => OsReplace.into(),
78+
["os", "replace"] => {
79+
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
80+
// set to non-default values.
81+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
82+
// ```text
83+
// 0 1 2 3
84+
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
85+
// ```
86+
if is_argument_non_default(&call.arguments, "src_dir_fd", 2)
87+
|| is_argument_non_default(&call.arguments, "dst_dir_fd", 3)
88+
{
89+
return;
90+
}
91+
OsReplace.into()
92+
}
5893
// PTH106
59-
["os", "rmdir"] => OsRmdir.into(),
94+
["os", "rmdir"] => {
95+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
96+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir)
97+
// ```text
98+
// 0 1
99+
// os.rmdir(path, *, dir_fd=None)
100+
// ```
101+
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
102+
return;
103+
}
104+
OsRmdir.into()
105+
}
60106
// PTH107
61-
["os", "remove"] => OsRemove.into(),
107+
["os", "remove"] => {
108+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
109+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove)
110+
// ```text
111+
// 0 1
112+
// os.remove(path, *, dir_fd=None)
113+
// ```
114+
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
115+
return;
116+
}
117+
OsRemove.into()
118+
}
62119
// PTH108
63-
["os", "unlink"] => OsUnlink.into(),
120+
["os", "unlink"] => {
121+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
122+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink)
123+
// ```text
124+
// 0 1
125+
// os.unlink(path, *, dir_fd=None)
126+
// ```
127+
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
128+
return;
129+
}
130+
OsUnlink.into()
131+
}
64132
// PTH109
65133
["os", "getcwd"] => OsGetcwd.into(),
66134
["os", "getcwdb"] => OsGetcwd.into(),
@@ -76,10 +144,17 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
76144
["os", "path", "islink"] => OsPathIslink.into(),
77145
// PTH116
78146
["os", "stat"] => {
147+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
148+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.stat)
149+
// ```text
150+
// 0 1 2
151+
// os.stat(path, *, dir_fd=None, follow_symlinks=True)
152+
// ```
79153
if call
80154
.arguments
81-
.find_positional(0)
155+
.find_argument_value("path", 0)
82156
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
157+
|| is_argument_non_default(&call.arguments, "dir_fd", 1)
83158
{
84159
return;
85160
}
@@ -148,13 +223,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
148223
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
149224
)
150225
})
226+
|| is_argument_non_default(&call.arguments, "opener", 7)
151227
|| call
152228
.arguments
153-
.find_argument_value("opener", 7)
154-
.is_some_and(|expr| !expr.is_none_literal_expr())
155-
|| call
156-
.arguments
157-
.find_positional(0)
229+
.find_argument_value("file", 0)
158230
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
159231
{
160232
return;
@@ -164,17 +236,53 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
164236
// PTH124
165237
["py", "path", "local"] => PyPath.into(),
166238
// PTH207
167-
["glob", "glob"] => Glob {
168-
function: "glob".to_string(),
239+
["glob", "glob"] => {
240+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
241+
// Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.glob)
242+
// ```text
243+
// 0 1 2 3 4
244+
// glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
245+
// ```
246+
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
247+
return;
248+
}
249+
250+
Glob {
251+
function: "glob".to_string(),
252+
}
253+
.into()
169254
}
170-
.into(),
171-
["glob", "iglob"] => Glob {
172-
function: "iglob".to_string(),
255+
256+
["glob", "iglob"] => {
257+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
258+
// Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.iglob)
259+
// ```text
260+
// 0 1 2 3 4
261+
// glob.iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
262+
// ```
263+
if is_argument_non_default(&call.arguments, "dir_fd", 2) {
264+
return;
265+
}
266+
267+
Glob {
268+
function: "iglob".to_string(),
269+
}
270+
.into()
173271
}
174-
.into(),
175272
// PTH115
176273
// Python 3.9+
177-
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => OsReadlink.into(),
274+
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
275+
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
276+
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink)
277+
// ```text
278+
// 0 1
279+
// os.readlink(path, *, dir_fd=None)
280+
// ```
281+
if is_argument_non_default(&call.arguments, "dir_fd", 1) {
282+
return;
283+
}
284+
OsReadlink.into()
285+
}
178286
// PTH208
179287
["os", "listdir"] => {
180288
if call
@@ -224,3 +332,10 @@ fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
224332
_ => None,
225333
}
226334
}
335+
336+
/// Returns `true` if argument `name` is set to a non-default `None` value.
337+
fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
338+
arguments
339+
.find_argument_value(name, position)
340+
.is_some_and(|expr| !expr.is_none_literal_expr())
341+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
3-
snapshot_kind: text
43
---
54
PTH207.py:9:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob`
65
|
@@ -26,4 +25,6 @@ PTH207.py:11:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob`
2625
10 | list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")))
2726
11 | search("*.png")
2827
| ^^^^^^ PTH207
28+
12 |
29+
13 | # if `dir_fd` is set, suppress the diagnostic
2930
|

0 commit comments

Comments
 (0)