[flake8-use-pathlib] Add autofix for PTH101, PTH104, PTH105, PTH121 (#19404)

## Summary

Part of https://github.com/astral-sh/ruff/issues/2331

## Test Plan

`cargo nextest run flake8_use_pathlib`
This commit is contained in:
chiri 2025-07-23 19:13:43 +03:00 committed by GitHub
parent 1dcef1a011
commit 89258f1938
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 666 additions and 283 deletions

View file

@ -47,3 +47,19 @@ def _():
from builtin import open from builtin import open
with open(p) as _: ... # No error with open(p) as _: ... # No error
file = "file_1.py"
rename(file, "file_2.py")
rename(
# commment 1
file, # comment 2
"file_2.py"
,
# comment 3
)
rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
rename(file, "file_2.py", src_dir_fd=1)

View file

@ -1039,14 +1039,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call); flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
} }
if checker.any_rule_enabled(&[ if checker.any_rule_enabled(&[
Rule::OsChmod,
Rule::OsMkdir, Rule::OsMkdir,
Rule::OsMakedirs, Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsStat, Rule::OsStat,
Rule::OsPathJoin, Rule::OsPathJoin,
Rule::OsPathSamefile,
Rule::OsPathSplitext, Rule::OsPathSplitext,
Rule::BuiltinOpen, Rule::BuiltinOpen,
Rule::PyPath, Rule::PyPath,
@ -1112,6 +1108,18 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsGetcwd) { if checker.is_rule_enabled(Rule::OsGetcwd) {
flake8_use_pathlib::rules::os_getcwd(checker, call, segments); flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
} }
if checker.is_rule_enabled(Rule::OsChmod) {
flake8_use_pathlib::rules::os_chmod(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsRename) {
flake8_use_pathlib::rules::os_rename(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsReplace) {
flake8_use_pathlib::rules::os_replace(checker, call, segments);
}
if checker.is_rule_enabled(Rule::OsPathSamefile) {
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) { if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory( flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments, checker, call, segments,

View file

@ -920,11 +920,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-use-pathlib // flake8-use-pathlib
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath), (Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod), (Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir), (Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs), (Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename), (Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace), (Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir), (Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove), (Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink), (Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
@ -940,7 +940,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin), (Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename), (Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname), (Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile), (Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext), (Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen), (Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath), (Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),

View file

@ -134,6 +134,26 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
settings.preview.is_enabled() settings.preview.is_enabled()
} }
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_chmod_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_rename_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_replace_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19404
pub(crate) const fn is_fix_os_path_samefile_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19245 // https://github.com/astral-sh/ruff/pull/19245
pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool { pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()

View file

@ -1,8 +1,8 @@
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::ImportRequest; use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation}; use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{self as ast}; use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_ast::{Expr, ExprCall}; use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool { pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
@ -72,3 +72,85 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
}); });
} }
} }
pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
match expr {
Expr::Name(name) => Some(name),
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
_ => None,
}
}
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
if matches!(
expr,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
..
})
) {
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_int(binding, semantic)
}
pub(crate) fn check_os_pathlib_two_arg_calls(
checker: &Checker,
call: &ExprCall,
attr: &str,
path_arg: &str,
second_arg: &str,
fix_enabled: bool,
violation: impl Violation,
) {
let range = call.range();
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
let (Some(path_expr), Some(second_expr)) = (
call.arguments.find_argument_value(path_arg, 0),
call.arguments.find_argument_value(second_arg, 1),
) else {
return;
};
let path_code = checker.locator().slice(path_expr.range());
let second_code = checker.locator().slice(second_expr.range());
if fix_enabled {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_pathlib_path_call(checker, path_expr) {
format!("{path_code}.{attr}({second_code})")
} else {
format!("{binding}({path_code}).{attr}({second_code})")
};
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}
}

View file

@ -1,5 +1,6 @@
pub(crate) use glob_rule::*; pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*; pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_chmod::*;
pub(crate) use os_getcwd::*; pub(crate) use os_getcwd::*;
pub(crate) use os_path_abspath::*; pub(crate) use os_path_abspath::*;
pub(crate) use os_path_basename::*; pub(crate) use os_path_basename::*;
@ -14,8 +15,11 @@ pub(crate) use os_path_isabs::*;
pub(crate) use os_path_isdir::*; pub(crate) use os_path_isdir::*;
pub(crate) use os_path_isfile::*; pub(crate) use os_path_isfile::*;
pub(crate) use os_path_islink::*; pub(crate) use os_path_islink::*;
pub(crate) use os_path_samefile::*;
pub(crate) use os_readlink::*; pub(crate) use os_readlink::*;
pub(crate) use os_remove::*; pub(crate) use os_remove::*;
pub(crate) use os_rename::*;
pub(crate) use os_replace::*;
pub(crate) use os_rmdir::*; pub(crate) use os_rmdir::*;
pub(crate) use os_sep_split::*; pub(crate) use os_sep_split::*;
pub(crate) use os_unlink::*; pub(crate) use os_unlink::*;
@ -24,6 +28,7 @@ pub(crate) use replaceable_by_pathlib::*;
mod glob_rule; mod glob_rule;
mod invalid_pathlib_with_suffix; mod invalid_pathlib_with_suffix;
mod os_chmod;
mod os_getcwd; mod os_getcwd;
mod os_path_abspath; mod os_path_abspath;
mod os_path_basename; mod os_path_basename;
@ -38,8 +43,11 @@ mod os_path_isabs;
mod os_path_isdir; mod os_path_isdir;
mod os_path_isfile; mod os_path_isfile;
mod os_path_islink; mod os_path_islink;
mod os_path_samefile;
mod os_readlink; mod os_readlink;
mod os_remove; mod os_remove;
mod os_rename;
mod os_replace;
mod os_rmdir; mod os_rmdir;
mod os_sep_split; mod os_sep_split;
mod os_unlink; mod os_unlink;

View file

@ -0,0 +1,94 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_chmod_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.chmod`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.chmod()` can improve readability over the `os`
/// module's counterparts (e.g., `os.chmod()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.chmod("file.py", 0o444)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").chmod(0o444)
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsChmod;
impl Violation for OsChmod {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.chmod()` should be replaced by `Path.chmod()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).chmod(...)`".to_string())
}
}
/// PTH101
pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "chmod"] {
return;
}
// `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_keyword_only_argument_non_default(&call.arguments, "dir_fd")
{
return;
}
check_os_pathlib_two_arg_calls(
checker,
call,
"chmod",
"path",
"mode",
is_fix_os_chmod_enabled(checker.settings()),
OsChmod,
);
}

View file

@ -0,0 +1,77 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_samefile_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.path.samefile`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.samefile()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.samefile()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.samefile("f1.py", "f2.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("f1.py").samefile("f2.py")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathSamefile;
impl Violation for OsPathSamefile {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.samefile()` should be replaced by `Path.samefile()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).samefile()`".to_string())
}
}
/// PTH121
pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "path", "samefile"] {
return;
}
check_os_pathlib_two_arg_calls(
checker,
call,
"samefile",
"f1",
"f2",
is_fix_os_path_samefile_enabled(checker.settings()),
OsPathSamefile,
);
}

View file

@ -0,0 +1,91 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rename_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.rename`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.rename()` can improve readability over the `os`
/// module's counterparts (e.g., `os.rename()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.rename("old.py", "new.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("old.py").rename("new.py")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRename;
impl Violation for OsRename {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.rename()` should be replaced by `Path.rename()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).rename(...)`".to_string())
}
}
/// PTH104
pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "rename"] {
return;
}
// `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.rename)
// ```text
// 0 1 2 3
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
{
return;
}
check_os_pathlib_two_arg_calls(
checker,
call,
"rename",
"src",
"dst",
is_fix_os_rename_enabled(checker.settings()),
OsRename,
);
}

View file

@ -0,0 +1,94 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_replace_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
/// ## What it does
/// Checks for uses of `os.replace`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.replace()` can improve readability over the `os`
/// module's counterparts (e.g., `os.replace()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
/// e.g., in hot loops.
///
/// ## Examples
/// ```python
/// import os
///
/// os.replace("old.py", "new.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("old.py").replace("new.py")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsReplace;
impl Violation for OsReplace {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.replace()` should be replaced by `Path.replace()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).replace(...)`".to_string())
}
}
/// PTH105
pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str]) {
if segments != ["os", "replace"] {
return;
}
// `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_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
{
return;
}
check_os_pathlib_two_arg_calls(
checker,
call,
"replace",
"src",
"dst",
is_fix_os_replace_enabled(checker.settings()),
OsReplace,
);
}

View file

@ -1,14 +1,16 @@
use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall}; use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default; use crate::rules::flake8_use_pathlib::helpers::{
use crate::rules::flake8_use_pathlib::rules::Glob; is_file_descriptor, is_keyword_only_argument_non_default,
use crate::rules::flake8_use_pathlib::violations::{ };
BuiltinOpen, Joiner, OsChmod, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSamefile, use crate::rules::flake8_use_pathlib::{
OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath, rules::Glob,
violations::{
BuiltinOpen, Joiner, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSplitext, OsStat,
OsSymlink, PyPath,
},
}; };
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
@ -18,24 +20,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
let range = call.func.range(); let range = call.func.range();
match qualified_name.segments() { match qualified_name.segments() {
// PTH101
["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_keyword_only_argument_non_default(&call.arguments, "dir_fd")
{
return;
}
checker.report_diagnostic_if_enabled(OsChmod, range)
}
// PTH102 // PTH102
["os", "makedirs"] => checker.report_diagnostic_if_enabled(OsMakedirs, range), ["os", "makedirs"] => checker.report_diagnostic_if_enabled(OsMakedirs, range),
// PTH103 // PTH103
@ -51,38 +35,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
} }
checker.report_diagnostic_if_enabled(OsMkdir, range) checker.report_diagnostic_if_enabled(OsMkdir, range)
} }
// PTH104
["os", "rename"] => {
// `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.rename)
// ```text
// 0 1 2 3
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
// ```
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
{
return;
}
checker.report_diagnostic_if_enabled(OsRename, range)
}
// PTH105
["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_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
{
return;
}
checker.report_diagnostic_if_enabled(OsReplace, range)
}
// PTH116 // PTH116
["os", "stat"] => { ["os", "stat"] => {
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values. // `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
@ -124,8 +76,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}, },
range, range,
), ),
// PTH121
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
// PTH122 // PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range), ["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH211 // PTH211
@ -234,37 +184,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
}; };
} }
/// 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!(
expr,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
..
})
) {
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_int(binding, semantic)
}
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
match expr {
Expr::Name(name) => Some(name),
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
_ => None,
}
}
/// Returns `true` if argument `name` is set to a non-default `None` value. /// 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 { fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
arguments arguments

View file

@ -20,6 +20,7 @@ full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
9 | aaa = os.mkdir(p) 9 | aaa = os.mkdir(p)
10 | os.makedirs(p) 10 | os.makedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -50,6 +51,7 @@ full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
12 | os.replace(p) 12 | os.replace(p)
13 | os.rmdir(p) 13 | os.rmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()` full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -60,6 +62,7 @@ full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
13 | os.rmdir(p) 13 | os.rmdir(p)
14 | os.remove(p) 14 | os.remove(p)
| |
= help: Replace with `Path(...).replace(...)`
full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -253,6 +256,7 @@ full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
31 | os.path.splitext(p) 31 | os.path.splitext(p)
32 | with open(p) as fp: 32 | with open(p) as fp:
| |
= help: Replace with `Path(...).samefile()`
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |

View file

@ -20,6 +20,7 @@ import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
9 | aaa = foo.mkdir(p) 9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p) 10 | foo.makedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -50,6 +51,7 @@ import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
12 | foo.replace(p) 12 | foo.replace(p)
13 | foo.rmdir(p) 13 | foo.rmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()` import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -60,6 +62,7 @@ import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
13 | foo.rmdir(p) 13 | foo.rmdir(p)
14 | foo.remove(p) 14 | foo.remove(p)
| |
= help: Replace with `Path(...).replace(...)`
import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -252,6 +255,7 @@ import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
| ^^^^^^^^^^^^^^ PTH121 | ^^^^^^^^^^^^^^ PTH121
31 | foo_p.splitext(p) 31 | foo_p.splitext(p)
| |
= help: Replace with `Path(...).samefile()`
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |

View file

@ -20,6 +20,7 @@ import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
11 | aaa = mkdir(p) 11 | aaa = mkdir(p)
12 | makedirs(p) 12 | makedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -50,6 +51,7 @@ import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
14 | replace(p) 14 | replace(p)
15 | rmdir(p) 15 | rmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()` import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -60,6 +62,7 @@ import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()
15 | rmdir(p) 15 | rmdir(p)
16 | remove(p) 16 | remove(p)
| |
= help: Replace with `Path(...).replace(...)`
import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -253,6 +256,7 @@ import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.sam
33 | splitext(p) 33 | splitext(p)
34 | with open(p) as fp: 34 | with open(p) as fp:
| |
= help: Replace with `Path(...).samefile()`
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |
@ -289,3 +293,36 @@ import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
43 | with open(p) as _: ... # Error 43 | with open(p) as _: ... # Error
| ^^^^ PTH123 | ^^^^ PTH123
| |
import_from.py:53:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
51 | file = "file_1.py"
52 |
53 | rename(file, "file_2.py")
| ^^^^^^ PTH104
54 |
55 | rename(
|
= help: Replace with `Path(...).rename(...)`
import_from.py:55:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
53 | rename(file, "file_2.py")
54 |
55 | rename(
| ^^^^^^ PTH104
56 | # commment 1
57 | file, # comment 2
|
= help: Replace with `Path(...).rename(...)`
import_from.py:63:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
61 | )
62 |
63 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
| ^^^^^^ PTH104
64 |
65 | rename(file, "file_2.py", src_dir_fd=1)
|
= help: Replace with `Path(...).rename(...)`

View file

@ -20,6 +20,7 @@ import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
16 | aaa = xmkdir(p) 16 | aaa = xmkdir(p)
17 | xmakedirs(p) 17 | xmakedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -50,6 +51,7 @@ import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename(
19 | xreplace(p) 19 | xreplace(p)
20 | xrmdir(p) 20 | xrmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()` import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -60,6 +62,7 @@ import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replac
20 | xrmdir(p) 20 | xrmdir(p)
21 | xremove(p) 21 | xremove(p)
| |
= help: Replace with `Path(...).replace(...)`
import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()` import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -252,6 +255,7 @@ import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.
| ^^^^^^^^^ PTH121 | ^^^^^^^^^ PTH121
38 | xsplitext(p) 38 | xsplitext(p)
| |
= help: Replace with `Path(...).samefile()`
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |

View file

@ -34,6 +34,7 @@ full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
9 | aaa = os.mkdir(p) 9 | aaa = os.mkdir(p)
10 | os.makedirs(p) 10 | os.makedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -64,6 +65,7 @@ full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
12 | os.replace(p) 12 | os.replace(p)
13 | os.rmdir(p) 13 | os.rmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()` full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -74,6 +76,7 @@ full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
13 | os.rmdir(p) 13 | os.rmdir(p)
14 | os.remove(p) 14 | os.remove(p)
| |
= help: Replace with `Path(...).replace(...)`
full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -471,6 +474,7 @@ full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
31 | os.path.splitext(p) 31 | os.path.splitext(p)
32 | with open(p) as fp: 32 | with open(p) as fp:
| |
= help: Replace with `Path(...).samefile()`
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |

View file

@ -34,6 +34,7 @@ import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
9 | aaa = foo.mkdir(p) 9 | aaa = foo.mkdir(p)
10 | foo.makedirs(p) 10 | foo.makedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -64,6 +65,7 @@ import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
12 | foo.replace(p) 12 | foo.replace(p)
13 | foo.rmdir(p) 13 | foo.rmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()` import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -74,6 +76,7 @@ import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
13 | foo.rmdir(p) 13 | foo.rmdir(p)
14 | foo.remove(p) 14 | foo.remove(p)
| |
= help: Replace with `Path(...).replace(...)`
import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -469,6 +472,7 @@ import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
| ^^^^^^^^^^^^^^ PTH121 | ^^^^^^^^^^^^^^ PTH121
31 | foo_p.splitext(p) 31 | foo_p.splitext(p)
| |
= help: Replace with `Path(...).samefile()`
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |

View file

@ -35,6 +35,7 @@ import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
11 | aaa = mkdir(p) 11 | aaa = mkdir(p)
12 | makedirs(p) 12 | makedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -65,6 +66,7 @@ import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
14 | replace(p) 14 | replace(p)
15 | rmdir(p) 15 | rmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()` import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -75,6 +77,7 @@ import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()
15 | rmdir(p) 15 | rmdir(p)
16 | remove(p) 16 | remove(p)
| |
= help: Replace with `Path(...).replace(...)`
import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -484,6 +487,7 @@ import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.sam
33 | splitext(p) 33 | splitext(p)
34 | with open(p) as fp: 34 | with open(p) as fp:
| |
= help: Replace with `Path(...).samefile()`
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |
@ -520,3 +524,95 @@ import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
43 | with open(p) as _: ... # Error 43 | with open(p) as _: ... # Error
| ^^^^ PTH123 | ^^^^ PTH123
| |
import_from.py:53:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
51 | file = "file_1.py"
52 |
53 | rename(file, "file_2.py")
| ^^^^^^ PTH104
54 |
55 | rename(
|
= help: Replace with `Path(...).rename(...)`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
50 51 |
51 52 | file = "file_1.py"
52 53 |
53 |-rename(file, "file_2.py")
54 |+pathlib.Path(file).rename("file_2.py")
54 55 |
55 56 | rename(
56 57 | # commment 1
import_from.py:55:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
53 | rename(file, "file_2.py")
54 |
55 | rename(
| ^^^^^^ PTH104
56 | # commment 1
57 | file, # comment 2
|
= help: Replace with `Path(...).rename(...)`
Unsafe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
52 53 |
53 54 | rename(file, "file_2.py")
54 55 |
55 |-rename(
56 |- # commment 1
57 |- file, # comment 2
58 |- "file_2.py"
59 |- ,
60 |- # comment 3
61 |-)
56 |+pathlib.Path(file).rename("file_2.py")
62 57 |
63 58 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
64 59 |
import_from.py:63:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
61 | )
62 |
63 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
| ^^^^^^ PTH104
64 |
65 | rename(file, "file_2.py", src_dir_fd=1)
|
= help: Replace with `Path(...).rename(...)`
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 |+import pathlib
5 6 |
6 7 | p = "/foo"
7 8 | q = "bar"
--------------------------------------------------------------------------------
60 61 | # comment 3
61 62 | )
62 63 |
63 |-rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
64 |+pathlib.Path(file).rename("file_2.py")
64 65 |
65 66 | rename(file, "file_2.py", src_dir_fd=1)

View file

@ -35,6 +35,7 @@ import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
16 | aaa = xmkdir(p) 16 | aaa = xmkdir(p)
17 | xmakedirs(p) 17 | xmakedirs(p)
| |
= help: Replace with `Path(...).chmod(...)`
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()` import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
| |
@ -65,6 +66,7 @@ import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename(
19 | xreplace(p) 19 | xreplace(p)
20 | xrmdir(p) 20 | xrmdir(p)
| |
= help: Replace with `Path(...).rename(...)`
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()` import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
| |
@ -75,6 +77,7 @@ import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replac
20 | xrmdir(p) 20 | xrmdir(p)
21 | xremove(p) 21 | xremove(p)
| |
= help: Replace with `Path(...).replace(...)`
import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()` import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
| |
@ -482,6 +485,7 @@ import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.
| ^^^^^^^^^ PTH121 | ^^^^^^^^^ PTH121
38 | xsplitext(p) 38 | xsplitext(p)
| |
= help: Replace with `Path(...).samefile()`
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
| |

View file

@ -2,51 +2,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation; use crate::Violation;
/// ## What it does
/// Checks for uses of `os.chmod`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.chmod()` can improve readability over the `os`
/// module's counterparts (e.g., `os.chmod()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.chmod("file.py", 0o444)
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("file.py").chmod(0o444)
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsChmod;
impl Violation for OsChmod {
#[derive_message_formats]
fn message(&self) -> String {
"`os.chmod()` should be replaced by `Path.chmod()`".to_string()
}
}
/// ## What it does /// ## What it does
/// Checks for uses of `os.makedirs`. /// Checks for uses of `os.makedirs`.
/// ///
@ -137,99 +92,6 @@ impl Violation for OsMkdir {
} }
} }
/// ## What it does
/// Checks for uses of `os.rename`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.rename()` can improve readability over the `os`
/// module's counterparts (e.g., `os.rename()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.rename("old.py", "new.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("old.py").rename("new.py")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsRename;
impl Violation for OsRename {
#[derive_message_formats]
fn message(&self) -> String {
"`os.rename()` should be replaced by `Path.rename()`".to_string()
}
}
/// ## What it does
/// Checks for uses of `os.replace`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`. When possible, using `Path` object
/// methods such as `Path.replace()` can improve readability over the `os`
/// module's counterparts (e.g., `os.replace()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
/// e.g., in hot loops.
///
/// ## Examples
/// ```python
/// import os
///
/// os.replace("old.py", "new.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("old.py").replace("new.py")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsReplace;
impl Violation for OsReplace {
#[derive_message_formats]
fn message(&self) -> String {
"`os.replace()` should be replaced by `Path.replace()`".to_string()
}
}
/// ## What it does /// ## What it does
/// Checks for uses of `os.stat`. /// Checks for uses of `os.stat`.
/// ///
@ -347,51 +209,6 @@ pub(crate) enum Joiner {
Joinpath, Joinpath,
} }
/// ## What it does
/// Checks for uses of `os.path.samefile`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.samefile()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.samefile()`).
///
/// ## Examples
/// ```python
/// import os
///
/// os.path.samefile("f1.py", "f2.py")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// Path("f1.py").samefile("f2.py")
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathSamefile;
impl Violation for OsPathSamefile {
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.samefile()` should be replaced by `Path.samefile()`".to_string()
}
}
/// ## What it does /// ## What it does
/// Checks for uses of `os.path.splitext`. /// Checks for uses of `os.path.splitext`.
/// ///