diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py index 752e924b8b..0005327d83 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH207.py @@ -9,3 +9,7 @@ extensions_dir = "./extensions" glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp")) list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))) search("*.png") + +# if `dir_fd` is set, suppress the diagnostic +glob.glob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1) +list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"), dir_fd=1)) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py index 0f99bc4b85..04bc90b801 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/full_name.py @@ -87,3 +87,20 @@ def bar(x: int): os.rename("src", "dst", src_dir_fd=3, dst_dir_fd=4) os.rename("src", "dst", src_dir_fd=3) os.rename("src", "dst", dst_dir_fd=4) + +# if `dir_fd` is set, suppress the diagnostic +os.readlink(p, dir_fd=1) +os.stat(p, dir_fd=2) +os.unlink(p, dir_fd=3) +os.remove(p, dir_fd=4) +os.rmdir(p, dir_fd=5) +os.mkdir(p, dir_fd=6) +os.chmod(p, dir_fd=7) +# `chmod` can also receive a file descriptor in the first argument +os.chmod(8) +os.chmod(x) + +# if `src_dir_fd` or `dst_dir_fd` are set, suppress the diagnostic +os.replace("src", "dst", src_dir_fd=1, dst_dir_fd=2) +os.replace("src", "dst", src_dir_fd=1) +os.replace("src", "dst", dst_dir_fd=2) diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 14b724730f..bf1b3c0661 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -26,41 +26,109 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { // PTH100 ["os", "path", "abspath"] => OsPathAbspath.into(), // PTH101 - ["os", "chmod"] => OsChmod.into(), + ["os", "chmod"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod) + // ```text + // 0 1 2 3 + // os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True) + // ``` + if call + .arguments + .find_argument_value("path", 0) + .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + || is_argument_non_default(&call.arguments, "dir_fd", 2) + { + return; + } + OsChmod.into() + } // PTH102 ["os", "makedirs"] => OsMakedirs.into(), // PTH103 - ["os", "mkdir"] => OsMkdir.into(), + ["os", "mkdir"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.mkdir) + // ```text + // 0 1 2 + // os.mkdir(path, mode=0o777, *, dir_fd=None) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 2) { + return; + } + OsMkdir.into() + } // PTH104 ["os", "rename"] => { // `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are - // are set to non-default values. + // set to non-default values. // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename) // ```text // 0 1 2 3 // os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None) // ``` - if call - .arguments - .find_argument_value("src_dir_fd", 2) - .is_some_and(|expr| !expr.is_none_literal_expr()) - || call - .arguments - .find_argument_value("dst_dir_fd", 3) - .is_some_and(|expr| !expr.is_none_literal_expr()) + if is_argument_non_default(&call.arguments, "src_dir_fd", 2) + || is_argument_non_default(&call.arguments, "dst_dir_fd", 3) { return; } OsRename.into() } // PTH105 - ["os", "replace"] => OsReplace.into(), + ["os", "replace"] => { + // `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are + // set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace) + // ```text + // 0 1 2 3 + // os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None) + // ``` + if is_argument_non_default(&call.arguments, "src_dir_fd", 2) + || is_argument_non_default(&call.arguments, "dst_dir_fd", 3) + { + return; + } + OsReplace.into() + } // PTH106 - ["os", "rmdir"] => OsRmdir.into(), + ["os", "rmdir"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rmdir) + // ```text + // 0 1 + // os.rmdir(path, *, dir_fd=None) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 1) { + return; + } + OsRmdir.into() + } // PTH107 - ["os", "remove"] => OsRemove.into(), + ["os", "remove"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.remove) + // ```text + // 0 1 + // os.remove(path, *, dir_fd=None) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 1) { + return; + } + OsRemove.into() + } // PTH108 - ["os", "unlink"] => OsUnlink.into(), + ["os", "unlink"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.unlink) + // ```text + // 0 1 + // os.unlink(path, *, dir_fd=None) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 1) { + return; + } + OsUnlink.into() + } // PTH109 ["os", "getcwd"] => OsGetcwd.into(), ["os", "getcwdb"] => OsGetcwd.into(), @@ -76,10 +144,17 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { ["os", "path", "islink"] => OsPathIslink.into(), // PTH116 ["os", "stat"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.stat) + // ```text + // 0 1 2 + // os.stat(path, *, dir_fd=None, follow_symlinks=True) + // ``` if call .arguments - .find_positional(0) + .find_argument_value("path", 0) .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + || is_argument_non_default(&call.arguments, "dir_fd", 1) { return; } @@ -148,13 +223,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) ) }) + || is_argument_non_default(&call.arguments, "opener", 7) || call .arguments - .find_argument_value("opener", 7) - .is_some_and(|expr| !expr.is_none_literal_expr()) - || call - .arguments - .find_positional(0) + .find_argument_value("file", 0) .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) { return; @@ -164,17 +236,53 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { // PTH124 ["py", "path", "local"] => PyPath.into(), // PTH207 - ["glob", "glob"] => Glob { - function: "glob".to_string(), + ["glob", "glob"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.glob) + // ```text + // 0 1 2 3 4 + // glob.glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 2) { + return; + } + + Glob { + function: "glob".to_string(), + } + .into() } - .into(), - ["glob", "iglob"] => Glob { - function: "iglob".to_string(), + + ["glob", "iglob"] => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/glob.html#glob.iglob) + // ```text + // 0 1 2 3 4 + // glob.iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 2) { + return; + } + + Glob { + function: "iglob".to_string(), + } + .into() } - .into(), // PTH115 // Python 3.9+ - ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => OsReadlink.into(), + ["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => { + // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. + // Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.readlink) + // ```text + // 0 1 + // os.readlink(path, *, dir_fd=None) + // ``` + if is_argument_non_default(&call.arguments, "dir_fd", 1) { + return; + } + OsReadlink.into() + } // PTH208 ["os", "listdir"] => { if call @@ -224,3 +332,10 @@ fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { _ => None, } } + +/// 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()) +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap index 6d30e0fb5e..ddcba2ba59 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH207_PTH207.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs -snapshot_kind: text --- PTH207.py:9:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob` | @@ -26,4 +25,6 @@ PTH207.py:11:1: PTH207 Replace `glob` with `Path.glob` or `Path.rglob` 10 | list(glob.iglob(os.path.join(extensions_dir, "ops", "autograd", "*.cpp"))) 11 | search("*.png") | ^^^^^^ PTH207 +12 | +13 | # if `dir_fd` is set, suppress the diagnostic |