[flake8-use-pathlib] PTH* suppress diagnostic for all os.* functions that have the dir_fd parameter (#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 #17776.

This PR also handles all other `PTH*` rules that don't support file
descriptors.

## Test Plan

<!-- How was it tested? -->

Update existing tests.
This commit is contained in:
Victor Hugo Gomes 2025-05-12 17:11:56 -03:00 committed by GitHub
parent c9031ce59f
commit d7ef01401c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 166 additions and 29 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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())
}

View file

@ -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
|