diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH123.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH123.py new file mode 100644 index 0000000000..fccd676321 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH123.py @@ -0,0 +1,24 @@ +import builtins +from pathlib import Path + +_file = "file.txt" +_x = ("r+", -1) +r_plus = "r+" + +builtins.open(file=_file) + +open(_file, "r+ ", - 1) +open(mode="wb", file=_file) +open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +open(_file, "r+", - 1, None, None, None, True, None) +open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +open(_file, f" {r_plus} ", - 1) +open(buffering=- 1, file=_file, encoding= "utf-8") + +# Only diagnostic +open() +open(_file, *_x) +open(_file, "r+", unknown=True) +open(_file, "r+", closefd=False) +open(_file, "r+", None, None, None, None, None, None, None) \ No newline at end of file diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/import_from.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/import_from.py index 6f5b08f811..ee6b5484bb 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/import_from.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/import_from.py @@ -62,4 +62,4 @@ rename( rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None) -rename(file, "file_2.py", src_dir_fd=1) \ No newline at end of file +rename(file, "file_2.py", src_dir_fd=1) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 7beedc4438..97dc1052db 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1051,7 +1051,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { Rule::OsStat, Rule::OsPathJoin, Rule::OsPathSplitext, - Rule::BuiltinOpen, Rule::PyPath, Rule::Glob, Rule::OsListdir, @@ -1135,6 +1134,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::OsSymlink) { flake8_use_pathlib::rules::os_symlink(checker, call, segments); } + if checker.is_rule_enabled(Rule::BuiltinOpen) { + flake8_use_pathlib::rules::builtin_open(checker, call, segments); + } if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) { flake8_use_pathlib::rules::path_constructor_current_directory( checker, call, segments, diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 598b1466d9..eba1404f2b 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -944,7 +944,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname), (Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile), (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::rules::BuiltinOpen), (Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath), (Flake8UsePathlib, "201") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory), (Flake8UsePathlib, "202") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetsize), diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 015caf0801..2459fd1afc 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -223,3 +223,8 @@ pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled( pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/20169 +pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs index 937b6e587b..2bfbedcad5 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs @@ -205,3 +205,14 @@ pub(crate) fn has_unknown_keywords_or_starred_expr( None => true, }) } + +/// Returns `true` if argument `name` is set to a non-default `None` value. +pub(crate) 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/mod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs index 6042c440e9..f7f6c64a8d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs @@ -59,6 +59,7 @@ mod tests { #[test_case(Rule::PyPath, Path::new("py_path_1.py"))] #[test_case(Rule::PyPath, Path::new("py_path_2.py"))] + #[test_case(Rule::BuiltinOpen, Path::new("PTH123.py"))] #[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))] @@ -123,6 +124,7 @@ mod tests { Ok(()) } + #[test_case(Rule::BuiltinOpen, Path::new("PTH123.py"))] #[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs new file mode 100644 index 0000000000..e6802131c2 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/builtin_open.rs @@ -0,0 +1,191 @@ +use ruff_diagnostics::{Applicability, Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{ArgOrKeyword, Expr, ExprBooleanLiteral, ExprCall}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use crate::preview::is_fix_builtin_open_enabled; +use crate::rules::flake8_use_pathlib::helpers::{ + has_unknown_keywords_or_starred_expr, is_argument_non_default, is_file_descriptor, + is_pathlib_path_call, +}; +use crate::{FixAvailability, Violation}; + +/// ## What it does +/// Checks for uses of the `open()` builtin. +/// +/// ## Why is this bad? +/// `pathlib` offers a high-level API for path manipulation. When possible, +/// using `Path` object methods such as `Path.open()` can improve readability +/// over the `open` builtin. +/// +/// ## Examples +/// ```python +/// with open("f1.py", "wb") as fp: +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// with Path("f1.py").open("wb") as fp: +/// ... +/// ``` +/// +/// ## Known issues +/// While using `pathlib` can improve the readability and type safety of your code, +/// it can be less performant than working 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.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open) +/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) +/// - [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#corresponding-tools) +/// - [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 BuiltinOpen; + +impl Violation for BuiltinOpen { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + "`open()` should be replaced by `Path.open()`".to_string() + } + + fn fix_title(&self) -> Option { + Some("Replace with `Path.open()`".to_string()) + } +} + +// PTH123 +pub(crate) fn builtin_open(checker: &Checker, call: &ExprCall, segments: &[&str]) { + // `closefd` and `opener` are not supported by pathlib, so check if they + // are set to non-default values. + // https://github.com/astral-sh/ruff/issues/7620 + // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): + // ```text + // builtins.open( + // file, 0 + // mode='r', 1 + // buffering=-1, 2 + // encoding=None, 3 + // errors=None, 4 + // newline=None, 5 + // closefd=True, 6 <= not supported + // opener=None 7 <= not supported + // ) + // ``` + // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): + // ```text + // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) + // ``` + let file_arg = call.arguments.find_argument_value("file", 0); + + if call + .arguments + .find_argument_value("closefd", 6) + .is_some_and(|expr| { + !matches!( + expr, + Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) + ) + }) + || is_argument_non_default(&call.arguments, "opener", 7) + || file_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) + { + return; + } + + if !matches!(segments, ["" | "builtins", "open"]) { + return; + } + + let mut diagnostic = checker.report_diagnostic(BuiltinOpen, call.func.range()); + + if !is_fix_builtin_open_enabled(checker.settings()) { + return; + } + + let Some(file) = file_arg else { + return; + }; + + if has_unknown_keywords_or_starred_expr( + &call.arguments, + &[ + "file", + "mode", + "buffering", + "encoding", + "errors", + "newline", + "closefd", + "opener", + ], + ) { + return; + } + + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("pathlib", "Path"), + call.start(), + checker.semantic(), + )?; + + let locator = checker.locator(); + let file_code = locator.slice(file.range()); + + let args = |i: usize, arg: ArgOrKeyword| match arg { + ArgOrKeyword::Arg(expr) => { + if expr.range() == file.range() || i == 6 || i == 7 { + None + } else { + Some(locator.slice(expr.range())) + } + } + ArgOrKeyword::Keyword(kw) => match kw.arg.as_deref() { + Some("mode" | "buffering" | "encoding" | "errors" | "newline") => { + Some(locator.slice(kw)) + } + _ => None, + }, + }; + + let open_args = itertools::join( + call.arguments + .arguments_source_order() + .enumerate() + .filter_map(|(i, arg)| args(i, arg)), + ", ", + ); + + let replacement = if is_pathlib_path_call(checker, file) { + format!("{file_code}.open({open_args})") + } else { + format!("{binding}({file_code}).open({open_args})") + }; + + let range = call.range(); + + 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, + )) + }); +} diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs index 46273bc082..339d2c6a9e 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs @@ -1,3 +1,4 @@ +pub(crate) use builtin_open::*; pub(crate) use glob_rule::*; pub(crate) use invalid_pathlib_with_suffix::*; pub(crate) use os_chmod::*; @@ -29,6 +30,7 @@ pub(crate) use os_unlink::*; pub(crate) use path_constructor_current_directory::*; pub(crate) use replaceable_by_pathlib::*; +mod builtin_open; mod glob_rule; mod invalid_pathlib_with_suffix; mod os_chmod; 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 3c3341d3a7..5ecef9398c 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 @@ -1,4 +1,4 @@ -use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall}; +use ruff_python_ast::{Expr, ExprCall}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -7,7 +7,7 @@ use crate::rules::flake8_use_pathlib::helpers::{ }; use crate::rules::flake8_use_pathlib::{ rules::Glob, - violations::{BuiltinOpen, Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath}, + violations::{Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath}, }; pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { @@ -60,42 +60,6 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { ), // PTH122 ["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range), - // PTH123 - ["" | "builtins", "open"] => { - // `closefd` and `opener` are not supported by pathlib, so check if they - // are set to non-default values. - // https://github.com/astral-sh/ruff/issues/7620 - // Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open): - // ```text - // 0 1 2 3 4 5 - // open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, - // 6 7 - // closefd=True, opener=None) - // ^^^^ ^^^^ - // ``` - // For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open): - // ```text - // Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) - // ``` - if call - .arguments - .find_argument_value("closefd", 6) - .is_some_and(|expr| { - !matches!( - expr, - Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. }) - ) - }) - || is_argument_non_default(&call.arguments, "opener", 7) - || call - .arguments - .find_argument_value("file", 0) - .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) - { - return; - } - checker.report_diagnostic_if_enabled(BuiltinOpen, range) - } // PTH124 ["py", "path", "local"] => checker.report_diagnostic_if_enabled(PyPath, range), // PTH207 @@ -151,10 +115,3 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { _ => return, }; } - -/// 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__PTH123_PTH123.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH123_PTH123.py.snap new file mode 100644 index 0000000000..488f69210c --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__PTH123_PTH123.py.snap @@ -0,0 +1,143 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:8:1 + | + 6 | r_plus = "r+" + 7 | + 8 | builtins.open(file=_file) + | ^^^^^^^^^^^^^ + 9 | +10 | open(_file, "r+ ", - 1) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:10:1 + | + 8 | builtins.open(file=_file) + 9 | +10 | open(_file, "r+ ", - 1) + | ^^^^ +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:11:1 + | +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) + | ^^^^ +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:12:1 + | +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") + | ^^^^ +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:13:1 + | +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) + | ^^^^ +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:14:1 + | +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) + | ^^^^ +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:15:1 + | +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) + | ^^^^ +16 | open(_file, f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:16:1 + | +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) + | ^^^^ +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:17:1 + | +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + | ^^^^ +18 | +19 | # Only diagnostic + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:20:1 + | +19 | # Only diagnostic +20 | open() + | ^^^^ +21 | open(_file, *_x) +22 | open(_file, "r+", unknown=True) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:21:1 + | +19 | # Only diagnostic +20 | open() +21 | open(_file, *_x) + | ^^^^ +22 | open(_file, "r+", unknown=True) +23 | open(_file, "r+", closefd=False) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:22:1 + | +20 | open() +21 | open(_file, *_x) +22 | open(_file, "r+", unknown=True) + | ^^^^ +23 | open(_file, "r+", closefd=False) +24 | open(_file, "r+", None, None, None, None, None, None, None) + | +help: Replace with `Path.open()` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap index 84900549de..d63c06b68b 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__full_name.py.snap @@ -305,6 +305,7 @@ PTH123 `open()` should be replaced by `Path.open()` 33 | fp.read() 34 | open(p).close() | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> full_name.py:34:1 @@ -316,6 +317,7 @@ PTH123 `open()` should be replaced by `Path.open()` 35 | os.getcwdb(p) 36 | os.path.join(p, *q) | +help: Replace with `Path.open()` PTH109 `os.getcwd()` should be replaced by `Path.cwd()` --> full_name.py:35:1 @@ -360,6 +362,7 @@ PTH123 `open()` should be replaced by `Path.open()` 47 | open(p, 'r', - 1, None, None, None, True, None) 48 | open(p, 'r', - 1, None, None, None, False, opener) | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> full_name.py:47:1 @@ -370,6 +373,7 @@ PTH123 `open()` should be replaced by `Path.open()` | ^^^^ 48 | open(p, 'r', - 1, None, None, None, False, opener) | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> full_name.py:65:1 @@ -381,6 +385,7 @@ PTH123 `open()` should be replaced by `Path.open()` 66 | byte_str = b"bar" 67 | open(byte_str) | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> full_name.py:67:1 @@ -392,6 +397,7 @@ PTH123 `open()` should be replaced by `Path.open()` 68 | 69 | def bytes_str_func() -> bytes: | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> full_name.py:71:1 @@ -403,6 +409,7 @@ PTH123 `open()` should be replaced by `Path.open()` 72 | 73 | # https://github.com/astral-sh/ruff/issues/17693 | +help: Replace with `Path.open()` PTH109 `os.getcwd()` should be replaced by `Path.cwd()` --> full_name.py:108:1 diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap index 010c779654..4e06614274 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__import_from.py.snap @@ -305,6 +305,7 @@ PTH123 `open()` should be replaced by `Path.open()` 35 | fp.read() 36 | open(p).close() | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> import_from.py:36:1 @@ -314,6 +315,7 @@ PTH123 `open()` should be replaced by `Path.open()` 36 | open(p).close() | ^^^^ | +help: Replace with `Path.open()` PTH123 `open()` should be replaced by `Path.open()` --> import_from.py:43:10 @@ -323,6 +325,7 @@ PTH123 `open()` should be replaced by `Path.open()` 43 | with open(p) as _: ... # Error | ^^^^ | +help: Replace with `Path.open()` PTH104 `os.rename()` should be replaced by `Path.rename()` --> import_from.py:53:1 diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH123_PTH123.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH123_PTH123.py.snap new file mode 100644 index 0000000000..e6f769753e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH123_PTH123.py.snap @@ -0,0 +1,215 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +--- +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:8:1 + | + 6 | r_plus = "r+" + 7 | + 8 | builtins.open(file=_file) + | ^^^^^^^^^^^^^ + 9 | +10 | open(_file, "r+ ", - 1) + | +help: Replace with `Path.open()` +5 | _x = ("r+", -1) +6 | r_plus = "r+" +7 | + - builtins.open(file=_file) +8 + Path(_file).open() +9 | +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:10:1 + | + 8 | builtins.open(file=_file) + 9 | +10 | open(_file, "r+ ", - 1) + | ^^^^ +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") + | +help: Replace with `Path.open()` +7 | +8 | builtins.open(file=_file) +9 | + - open(_file, "r+ ", - 1) +10 + Path(_file).open("r+ ", - 1) +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:11:1 + | +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) + | ^^^^ +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) + | +help: Replace with `Path.open()` +8 | builtins.open(file=_file) +9 | +10 | open(_file, "r+ ", - 1) + - open(mode="wb", file=_file) +11 + Path(_file).open(mode="wb") +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:12:1 + | +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") + | ^^^^ +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) + | +help: Replace with `Path.open()` +9 | +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) + - open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +12 + Path(_file).open(mode="r+", buffering=-1, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:13:1 + | +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) + | ^^^^ +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) + | +help: Replace with `Path.open()` +10 | open(_file, "r+ ", - 1) +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") + - open(_file, "r+", - 1, None, None, None, True, None) +13 + Path(_file).open("r+", - 1, None, None, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:14:1 + | +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) + | ^^^^ +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) + | +help: Replace with `Path.open()` +11 | open(mode="wb", file=_file) +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) + - open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +14 + Path(_file).open("r+", -1, None, None, None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:15:1 + | +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) + | ^^^^ +16 | open(_file, f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + | +help: Replace with `Path.open()` +12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8") +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) + - open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +15 + Path(_file).open(mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") +18 | + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:16:1 + | +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) + | ^^^^ +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + | +help: Replace with `Path.open()` +13 | open(_file, "r+", - 1, None, None, None, True, None) +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) + - open(_file, f" {r_plus} ", - 1) +16 + Path(_file).open(f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") +18 | +19 | # Only diagnostic + +PTH123 [*] `open()` should be replaced by `Path.open()` + --> PTH123.py:17:1 + | +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) +17 | open(buffering=- 1, file=_file, encoding= "utf-8") + | ^^^^ +18 | +19 | # Only diagnostic + | +help: Replace with `Path.open()` +14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None) +15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None) +16 | open(_file, f" {r_plus} ", - 1) + - open(buffering=- 1, file=_file, encoding= "utf-8") +17 + Path(_file).open(buffering=- 1, encoding= "utf-8") +18 | +19 | # Only diagnostic +20 | open() + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:20:1 + | +19 | # Only diagnostic +20 | open() + | ^^^^ +21 | open(_file, *_x) +22 | open(_file, "r+", unknown=True) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:21:1 + | +19 | # Only diagnostic +20 | open() +21 | open(_file, *_x) + | ^^^^ +22 | open(_file, "r+", unknown=True) +23 | open(_file, "r+", closefd=False) + | +help: Replace with `Path.open()` + +PTH123 `open()` should be replaced by `Path.open()` + --> PTH123.py:22:1 + | +20 | open() +21 | open(_file, *_x) +22 | open(_file, "r+", unknown=True) + | ^^^^ +23 | open(_file, "r+", closefd=False) +24 | open(_file, "r+", None, None, None, None, None, None, None) + | +help: Replace with `Path.open()` diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap index aa92f3ee7f..9906739de9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_full_name.py.snap @@ -520,7 +520,7 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an 33 | fp.read() | -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:32:6 | 30 | os.path.samefile(p) @@ -530,8 +530,24 @@ PTH123 `open()` should be replaced by `Path.open()` 33 | fp.read() 34 | open(p).close() | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +30 | os.path.dirname(p) +31 | os.path.samefile(p) +32 | os.path.splitext(p) + - with open(p) as fp: +33 + with pathlib.Path(p).open() as fp: +34 | fp.read() +35 | open(p).close() +36 | os.getcwdb(p) -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:34:1 | 32 | with open(p) as fp: @@ -541,6 +557,22 @@ PTH123 `open()` should be replaced by `Path.open()` 35 | os.getcwdb(p) 36 | os.path.join(p, *q) | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +32 | os.path.splitext(p) +33 | with open(p) as fp: +34 | fp.read() + - open(p).close() +35 + pathlib.Path(p).open().close() +36 | os.getcwdb(p) +37 | os.path.join(p, *q) +38 | os.sep.join(p, *q) PTH109 `os.getcwd()` should be replaced by `Path.cwd()` --> full_name.py:35:1 @@ -575,7 +607,7 @@ PTH118 `os.sep.join()` should be replaced by `Path.joinpath()` 39 | # https://github.com/astral-sh/ruff/issues/7620 | -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:46:1 | 44 | open(p, closefd=False) @@ -585,8 +617,24 @@ PTH123 `open()` should be replaced by `Path.open()` 47 | open(p, 'r', - 1, None, None, None, True, None) 48 | open(p, 'r', - 1, None, None, None, False, opener) | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +44 | +45 | open(p, closefd=False) +46 | open(p, opener=opener) + - open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) +47 + pathlib.Path(p).open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) +48 | open(p, 'r', - 1, None, None, None, True, None) +49 | open(p, 'r', - 1, None, None, None, False, opener) +50 | -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:47:1 | 45 | open(p, opener=opener) @@ -595,8 +643,24 @@ PTH123 `open()` should be replaced by `Path.open()` | ^^^^ 48 | open(p, 'r', - 1, None, None, None, False, opener) | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +45 | open(p, closefd=False) +46 | open(p, opener=opener) +47 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) + - open(p, 'r', - 1, None, None, None, True, None) +48 + pathlib.Path(p).open('r', - 1, None, None, None) +49 | open(p, 'r', - 1, None, None, None, False, opener) +50 | +51 | # Cannot be upgraded `pathlib.Open` does not support fds -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:65:1 | 63 | open(f()) @@ -606,8 +670,24 @@ PTH123 `open()` should be replaced by `Path.open()` 66 | byte_str = b"bar" 67 | open(byte_str) | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +63 | return 1 +64 | open(f()) +65 | + - open(b"foo") +66 + pathlib.Path(b"foo").open() +67 | byte_str = b"bar" +68 | open(byte_str) +69 | -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:67:1 | 65 | open(b"foo") @@ -617,8 +697,24 @@ PTH123 `open()` should be replaced by `Path.open()` 68 | 69 | def bytes_str_func() -> bytes: | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +65 | +66 | open(b"foo") +67 | byte_str = b"bar" + - open(byte_str) +68 + pathlib.Path(byte_str).open() +69 | +70 | def bytes_str_func() -> bytes: +71 | return b"foo" -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> full_name.py:71:1 | 69 | def bytes_str_func() -> bytes: @@ -628,6 +724,22 @@ PTH123 `open()` should be replaced by `Path.open()` 72 | 73 | # https://github.com/astral-sh/ruff/issues/17693 | +help: Replace with `Path.open()` +1 | import os +2 | import os.path +3 + import pathlib +4 | +5 | p = "/foo" +6 | q = "bar" +-------------------------------------------------------------------------------- +69 | +70 | def bytes_str_func() -> bytes: +71 | return b"foo" + - open(bytes_str_func()) +72 + pathlib.Path(bytes_str_func()).open() +73 | +74 | # https://github.com/astral-sh/ruff/issues/17693 +75 | os.stat(1) PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()` --> full_name.py:108:1 diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap index c4da087955..236b00ff4c 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview_import_from.py.snap @@ -535,7 +535,7 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an 35 | fp.read() | -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> import_from.py:34:6 | 32 | samefile(p) @@ -545,8 +545,25 @@ PTH123 `open()` should be replaced by `Path.open()` 35 | fp.read() 36 | open(p).close() | +help: Replace with `Path.open()` +2 | from os import remove, unlink, getcwd, readlink, stat +3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 | from os.path import isabs, join, basename, dirname, samefile, splitext +5 + import pathlib +6 | +7 | p = "/foo" +8 | q = "bar" +-------------------------------------------------------------------------------- +32 | dirname(p) +33 | samefile(p) +34 | splitext(p) + - with open(p) as fp: +35 + with pathlib.Path(p).open() as fp: +36 | fp.read() +37 | open(p).close() +38 | -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> import_from.py:36:1 | 34 | with open(p) as fp: @@ -554,8 +571,25 @@ PTH123 `open()` should be replaced by `Path.open()` 36 | open(p).close() | ^^^^ | +help: Replace with `Path.open()` +2 | from os import remove, unlink, getcwd, readlink, stat +3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 | from os.path import isabs, join, basename, dirname, samefile, splitext +5 + import pathlib +6 | +7 | p = "/foo" +8 | q = "bar" +-------------------------------------------------------------------------------- +34 | splitext(p) +35 | with open(p) as fp: +36 | fp.read() + - open(p).close() +37 + pathlib.Path(p).open().close() +38 | +39 | +40 | # https://github.com/astral-sh/ruff/issues/15442 -PTH123 `open()` should be replaced by `Path.open()` +PTH123 [*] `open()` should be replaced by `Path.open()` --> import_from.py:43:10 | 41 | from builtins import open @@ -563,6 +597,23 @@ PTH123 `open()` should be replaced by `Path.open()` 43 | with open(p) as _: ... # Error | ^^^^ | +help: Replace with `Path.open()` +2 | from os import remove, unlink, getcwd, readlink, stat +3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink +4 | from os.path import isabs, join, basename, dirname, samefile, splitext +5 + import pathlib +6 | +7 | p = "/foo" +8 | q = "bar" +-------------------------------------------------------------------------------- +41 | def _(): +42 | from builtins import open +43 | + - with open(p) as _: ... # Error +44 + with pathlib.Path(p).open() as _: ... # Error +45 | +46 | +47 | def _(): PTH104 [*] `os.rename()` should be replaced by `Path.rename()` --> import_from.py:53:1 diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs index 7f95879a25..6108ff3a27 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs @@ -174,50 +174,6 @@ impl Violation for OsPathSplitext { } } -/// ## What it does -/// Checks for uses of the `open()` builtin. -/// -/// ## Why is this bad? -/// `pathlib` offers a high-level API for path manipulation. When possible, -/// using `Path` object methods such as `Path.open()` can improve readability -/// over the `open` builtin. -/// -/// ## Examples -/// ```python -/// with open("f1.py", "wb") as fp: -/// ... -/// ``` -/// -/// Use instead: -/// ```python -/// from pathlib import Path -/// -/// with Path("f1.py").open("wb") as fp: -/// ... -/// ``` -/// -/// ## Known issues -/// While using `pathlib` can improve the readability and type safety of your code, -/// it can be less performant than working directly with strings, -/// especially on older versions of Python. -/// -/// ## References -/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open) -/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open) -/// - [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#corresponding-tools) -/// - [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 BuiltinOpen; - -impl Violation for BuiltinOpen { - #[derive_message_formats] - fn message(&self) -> String { - "`open()` should be replaced by `Path.open()`".to_string() - } -} - /// ## What it does /// Checks for uses of the `py.path` library. ///