@@ -13,7 +13,7 @@ use crate::rules::flake8_use_pathlib::violations::{
13
13
BuiltinOpen , Joiner , OsChmod , OsGetcwd , OsListdir , OsMakedirs , OsMkdir , OsPathAbspath ,
14
14
OsPathBasename , OsPathDirname , OsPathExists , OsPathExpanduser , OsPathIsabs , OsPathIsdir ,
15
15
OsPathIsfile , OsPathIslink , OsPathJoin , OsPathSamefile , OsPathSplitext , OsReadlink , OsRemove ,
16
- OsRename , OsReplace , OsRmdir , OsStat , OsUnlink , PyPath ,
16
+ OsRename , OsReplace , OsRmdir , OsStat , OsSymlink , OsUnlink , PyPath ,
17
17
} ;
18
18
use ruff_python_ast:: PythonVersion ;
19
19
@@ -38,7 +38,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
38
38
. arguments
39
39
. find_argument_value ( "path" , 0 )
40
40
. is_some_and ( |expr| is_file_descriptor ( expr, checker. semantic ( ) ) )
41
- || is_argument_non_default ( & call. arguments , "dir_fd" , 2 )
41
+ || is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" )
42
42
{
43
43
return ;
44
44
}
@@ -54,7 +54,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
54
54
// 0 1 2
55
55
// os.mkdir(path, mode=0o777, *, dir_fd=None)
56
56
// ```
57
- if is_argument_non_default ( & call. arguments , "dir_fd" , 2 ) {
57
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
58
58
return ;
59
59
}
60
60
Diagnostic :: new ( OsMkdir , range)
@@ -68,8 +68,8 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
68
68
// 0 1 2 3
69
69
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
70
70
// ```
71
- if is_argument_non_default ( & call. arguments , "src_dir_fd" , 2 )
72
- || is_argument_non_default ( & call. arguments , "dst_dir_fd" , 3 )
71
+ if is_keyword_only_argument_non_default ( & call. arguments , "src_dir_fd" )
72
+ || is_keyword_only_argument_non_default ( & call. arguments , "dst_dir_fd" )
73
73
{
74
74
return ;
75
75
}
@@ -84,8 +84,8 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
84
84
// 0 1 2 3
85
85
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
86
86
// ```
87
- if is_argument_non_default ( & call. arguments , "src_dir_fd" , 2 )
88
- || is_argument_non_default ( & call. arguments , "dst_dir_fd" , 3 )
87
+ if is_keyword_only_argument_non_default ( & call. arguments , "src_dir_fd" )
88
+ || is_keyword_only_argument_non_default ( & call. arguments , "dst_dir_fd" )
89
89
{
90
90
return ;
91
91
}
@@ -99,7 +99,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
99
99
// 0 1
100
100
// os.rmdir(path, *, dir_fd=None)
101
101
// ```
102
- if is_argument_non_default ( & call. arguments , "dir_fd" , 1 ) {
102
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
103
103
return ;
104
104
}
105
105
Diagnostic :: new ( OsRmdir , range)
@@ -112,7 +112,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
112
112
// 0 1
113
113
// os.remove(path, *, dir_fd=None)
114
114
// ```
115
- if is_argument_non_default ( & call. arguments , "dir_fd" , 1 ) {
115
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
116
116
return ;
117
117
}
118
118
Diagnostic :: new ( OsRemove , range)
@@ -125,7 +125,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
125
125
// 0 1
126
126
// os.unlink(path, *, dir_fd=None)
127
127
// ```
128
- if is_argument_non_default ( & call. arguments , "dir_fd" , 1 ) {
128
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
129
129
return ;
130
130
}
131
131
Diagnostic :: new ( OsUnlink , range)
@@ -155,7 +155,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
155
155
. arguments
156
156
. find_argument_value ( "path" , 0 )
157
157
. is_some_and ( |expr| is_file_descriptor ( expr, checker. semantic ( ) ) )
158
- || is_argument_non_default ( & call. arguments , "dir_fd" , 1 )
158
+ || is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" )
159
159
{
160
160
return ;
161
161
}
@@ -202,6 +202,20 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
202
202
[ "os" , "path" , "getmtime" ] => Diagnostic :: new ( OsPathGetmtime , range) ,
203
203
// PTH205
204
204
[ "os" , "path" , "getctime" ] => Diagnostic :: new ( OsPathGetctime , range) ,
205
+ // PTH211
206
+ [ "os" , "symlink" ] => {
207
+ // `dir_fd` is not supported by pathlib, so check if there are non-default values.
208
+ // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.symlink)
209
+ // ```text
210
+ // 0 1 2 3
211
+ // os.symlink(src, dst, target_is_directory=False, *, dir_fd=None)
212
+ // ```
213
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
214
+ return ;
215
+ }
216
+ Diagnostic :: new ( OsSymlink , range)
217
+ }
218
+
205
219
// PTH123
206
220
[ "" | "builtins" , "open" ] => {
207
221
// `closefd` and `opener` are not supported by pathlib, so check if they are
@@ -248,7 +262,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
248
262
// 0 1 2 3 4
249
263
// glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
250
264
// ```
251
- if is_argument_non_default ( & call. arguments , "dir_fd" , 2 ) {
265
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
252
266
return ;
253
267
}
254
268
@@ -267,7 +281,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
267
281
// 0 1 2 3 4
268
282
// glob.iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False)
269
283
// ```
270
- if is_argument_non_default ( & call. arguments , "dir_fd" , 2 ) {
284
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
271
285
return ;
272
286
}
273
287
@@ -287,7 +301,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
287
301
// 0 1
288
302
// os.readlink(path, *, dir_fd=None)
289
303
// ```
290
- if is_argument_non_default ( & call. arguments , "dir_fd" , 1 ) {
304
+ if is_keyword_only_argument_non_default ( & call. arguments , "dir_fd" ) {
291
305
return ;
292
306
}
293
307
Diagnostic :: new ( OsReadlink , range)
@@ -303,6 +317,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
303
317
}
304
318
Diagnostic :: new ( OsListdir , range)
305
319
}
320
+
306
321
_ => return ,
307
322
} ;
308
323
@@ -348,3 +363,9 @@ fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usi
348
363
. find_argument_value ( name, position)
349
364
. is_some_and ( |expr| !expr. is_none_literal_expr ( ) )
350
365
}
366
+
367
+ fn is_keyword_only_argument_non_default ( arguments : & ast:: Arguments , name : & str ) -> bool {
368
+ arguments
369
+ . find_keyword ( name)
370
+ . is_some_and ( |keyword| !keyword. value . is_none_literal_expr ( ) )
371
+ }
0 commit comments