[flake8-use-pathlib] Fix PTH104false positive when rename is passed a file descriptor (#17712)

## Summary
Contains the same changes to the semantic type inference as
https://github.com/astral-sh/ruff/pull/17705.

Fixes #17694
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
Snapshot tests.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Victor Hugo Gomes 2025-05-01 11:01:17 -03:00 committed by GitHub
parent 41f3f21629
commit 3353d07938
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 23 deletions

View file

@ -82,3 +82,8 @@ os.stat(func())
def bar(x: int):
os.stat(x)
# https://github.com/astral-sh/ruff/issues/17694
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)

View file

@ -32,7 +32,27 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
// PTH103
["os", "mkdir"] => OsMkdir.into(),
// PTH104
["os", "rename"] => OsRename.into(),
["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.
// 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())
{
return;
}
OsRename.into()
}
// PTH105
["os", "replace"] => OsReplace.into(),
// PTH106
@ -135,7 +155,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|| call
.arguments
.find_positional(0)
.is_some_and(|expr| is_file_descriptor_or_bytes_str(expr, checker.semantic()))
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
}
@ -174,10 +194,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}
}
fn is_file_descriptor_or_bytes_str(expr: &Expr, semantic: &SemanticModel) -> bool {
is_file_descriptor(expr, semantic) || is_bytes_string(expr, semantic)
}
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
if matches!(
@ -201,23 +217,6 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
typing::is_int(binding, semantic)
}
/// Returns `true` if the given expression is a bytes string.
fn is_bytes_string(expr: &Expr, semantic: &SemanticModel) -> bool {
if matches!(expr, Expr::BytesLiteral(_)) {
return true;
}
let Some(name) = get_name_expr(expr) else {
return false;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return false;
};
typing::is_bytes(binding, semantic)
}
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
match expr {
Expr::Name(name) => Some(name),

View file

@ -317,3 +317,33 @@ full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()`
| ^^^^ PTH123
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
full_name.py:65:1: PTH123 `open()` should be replaced by `Path.open()`
|
63 | open(f())
64 |
65 | open(b"foo")
| ^^^^ PTH123
66 | byte_str = b"bar"
67 | open(byte_str)
|
full_name.py:67:1: PTH123 `open()` should be replaced by `Path.open()`
|
65 | open(b"foo")
66 | byte_str = b"bar"
67 | open(byte_str)
| ^^^^ PTH123
68 |
69 | def bytes_str_func() -> bytes:
|
full_name.py:71:1: PTH123 `open()` should be replaced by `Path.open()`
|
69 | def bytes_str_func() -> bytes:
70 | return b"foo"
71 | open(bytes_str_func())
| ^^^^ PTH123
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|