[flake8-use-pathlib] Expand PTH201 to check all PurePath subclasses (#19440)

## Summary

Fixes #19437
This commit is contained in:
Dan Parizher 2025-07-31 22:18:07 -04:00 committed by GitHub
parent 6a2d358d7a
commit b3a26a50ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 464 additions and 2 deletions

View file

@ -1,4 +1,4 @@
from pathlib import Path, PurePath from pathlib import Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath
from pathlib import Path as pth from pathlib import Path as pth
@ -68,3 +68,11 @@ Path(".", "folder")
PurePath(".", "folder") PurePath(".", "folder")
Path() Path()
from importlib.metadata import PackagePath
_ = PosixPath(".")
_ = PurePosixPath(".")
_ = WindowsPath(".")
_ = PureWindowsPath(".")
_ = PackagePath(".")

View file

@ -20,6 +20,35 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
}) })
} }
/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support.
/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for
/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`.
pub(crate) fn is_pure_path_subclass_with_preview(
checker: &crate::checkers::ast::Checker,
segments: &[&str],
) -> bool {
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
if is_core_pathlib {
return true;
}
if checker.settings().preview.is_enabled() {
let is_expanded_pathlib = matches!(
segments,
[
"pathlib",
"PosixPath" | "PurePosixPath" | "WindowsPath" | "PureWindowsPath"
]
);
let is_packagepath = matches!(segments, ["importlib", "metadata", "PackagePath"]);
return is_expanded_pathlib || is_packagepath;
}
false
}
/// We check functions that take only 1 argument, this does not apply to functions /// We check functions that take only 1 argument, this does not apply to functions
/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib, /// with `dir_fd` argument, because `dir_fd` is not supported by pathlib,
/// so check if it's set to non-default values /// so check if it's set to non-default values

View file

@ -123,6 +123,7 @@ mod tests {
Ok(()) Ok(())
} }
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))] #[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]

View file

@ -9,6 +9,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::edits::{Parentheses, remove_argument}; use crate::fix::edits::{Parentheses, remove_argument};
use crate::rules::flake8_use_pathlib::helpers::is_pure_path_subclass_with_preview;
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// ## What it does /// ## What it does
@ -69,7 +70,7 @@ pub(crate) fn path_constructor_current_directory(
let arguments = &call.arguments; let arguments = &call.arguments;
if !matches!(segments, ["pathlib", "Path" | "PurePath"]) { if !is_pure_path_subclass_with_preview(checker, segments) {
return; return;
} }

View file

@ -0,0 +1,423 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
assertion_line: 144
---
PTH201.py:6:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
5 | # match
6 | _ = Path(".")
| ^^^ PTH201
7 | _ = pth(".")
8 | _ = PurePath(".")
|
= help: Remove the current directory argument
Safe fix
3 3 |
4 4 |
5 5 | # match
6 |-_ = Path(".")
6 |+_ = Path()
7 7 | _ = pth(".")
8 8 | _ = PurePath(".")
9 9 | _ = Path("")
PTH201.py:7:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
5 | # match
6 | _ = Path(".")
7 | _ = pth(".")
| ^^^ PTH201
8 | _ = PurePath(".")
9 | _ = Path("")
|
= help: Remove the current directory argument
Safe fix
4 4 |
5 5 | # match
6 6 | _ = Path(".")
7 |-_ = pth(".")
7 |+_ = pth()
8 8 | _ = PurePath(".")
9 9 | _ = Path("")
10 10 |
PTH201.py:8:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
6 | _ = Path(".")
7 | _ = pth(".")
8 | _ = PurePath(".")
| ^^^ PTH201
9 | _ = Path("")
|
= help: Remove the current directory argument
Safe fix
5 5 | # match
6 6 | _ = Path(".")
7 7 | _ = pth(".")
8 |-_ = PurePath(".")
8 |+_ = PurePath()
9 9 | _ = Path("")
10 10 |
11 11 | Path('', )
PTH201.py:9:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
7 | _ = pth(".")
8 | _ = PurePath(".")
9 | _ = Path("")
| ^^ PTH201
10 |
11 | Path('', )
|
= help: Remove the current directory argument
Safe fix
6 6 | _ = Path(".")
7 7 | _ = pth(".")
8 8 | _ = PurePath(".")
9 |-_ = Path("")
9 |+_ = Path()
10 10 |
11 11 | Path('', )
12 12 |
PTH201.py:11:6: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
9 | _ = Path("")
10 |
11 | Path('', )
| ^^ PTH201
12 |
13 | Path(
|
= help: Remove the current directory argument
Safe fix
8 8 | _ = PurePath(".")
9 9 | _ = Path("")
10 10 |
11 |-Path('', )
11 |+Path()
12 12 |
13 13 | Path(
14 14 | '',
PTH201.py:14:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
13 | Path(
14 | '',
| ^^ PTH201
15 | )
|
= help: Remove the current directory argument
Safe fix
10 10 |
11 11 | Path('', )
12 12 |
13 |-Path(
14 |- '',
15 |-)
13 |+Path()
16 14 |
17 15 | Path( # Comment before argument
18 16 | '',
PTH201.py:18:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
17 | Path( # Comment before argument
18 | '',
| ^^ PTH201
19 | )
|
= help: Remove the current directory argument
Unsafe fix
14 14 | '',
15 15 | )
16 16 |
17 |-Path( # Comment before argument
18 |- '',
19 |-)
17 |+Path()
20 18 |
21 19 | Path(
22 20 | '', # EOL comment
PTH201.py:22:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
21 | Path(
22 | '', # EOL comment
| ^^ PTH201
23 | )
|
= help: Remove the current directory argument
Unsafe fix
18 18 | '',
19 19 | )
20 20 |
21 |-Path(
22 |- '', # EOL comment
23 |-)
21 |+Path()
24 22 |
25 23 | Path(
26 24 | '' # Comment in the middle of implicitly concatenated string
PTH201.py:26:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
25 | Path(
26 | / '' # Comment in the middle of implicitly concatenated string
27 | | ".",
| |_______^ PTH201
28 | )
|
= help: Remove the current directory argument
Unsafe fix
22 22 | '', # EOL comment
23 23 | )
24 24 |
25 |-Path(
26 |- '' # Comment in the middle of implicitly concatenated string
27 |- ".",
28 |-)
25 |+Path()
29 26 |
30 27 | Path(
31 28 | '' # Comment before comma
PTH201.py:31:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
30 | Path(
31 | '' # Comment before comma
| ^^ PTH201
32 | ,
33 | )
|
= help: Remove the current directory argument
Unsafe fix
27 27 | ".",
28 28 | )
29 29 |
30 |-Path(
31 |- '' # Comment before comma
32 |- ,
33 |-)
30 |+Path()
34 31 |
35 32 | Path(
36 33 | '',
PTH201.py:36:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
35 | Path(
36 | '',
| ^^ PTH201
37 | ) / "bare"
|
= help: Remove the current directory argument
Safe fix
33 33 | )
34 34 |
35 35 | Path(
36 |- '',
37 |-) / "bare"
36 |+ "bare",
37 |+)
38 38 |
39 39 | Path( # Comment before argument
40 40 | '',
PTH201.py:40:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
39 | Path( # Comment before argument
40 | '',
| ^^ PTH201
41 | ) / ("parenthesized")
|
= help: Remove the current directory argument
Unsafe fix
37 37 | ) / "bare"
38 38 |
39 39 | Path( # Comment before argument
40 |- '',
41 |-) / ("parenthesized")
40 |+ ("parenthesized"),
41 |+)
42 42 |
43 43 | Path(
44 44 | '', # EOL comment
PTH201.py:44:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
43 | Path(
44 | '', # EOL comment
| ^^ PTH201
45 | ) / ( ("double parenthesized" ) )
|
= help: Remove the current directory argument
Unsafe fix
41 41 | ) / ("parenthesized")
42 42 |
43 43 | Path(
44 |- '', # EOL comment
45 |-) / ( ("double parenthesized" ) )
44 |+ ( ("double parenthesized" ) ), # EOL comment
45 |+)
46 46 |
47 47 | ( Path(
48 48 | '' # Comment in the middle of implicitly concatenated string
PTH201.py:48:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
47 | ( Path(
48 | / '' # Comment in the middle of implicitly concatenated string
49 | | ".",
| |_______^ PTH201
50 | ) )/ (("parenthesized path call")
51 | # Comment between closing parentheses
|
= help: Remove the current directory argument
Unsafe fix
44 44 | '', # EOL comment
45 45 | ) / ( ("double parenthesized" ) )
46 46 |
47 |-( Path(
48 |- '' # Comment in the middle of implicitly concatenated string
49 |- ".",
50 |-) )/ (("parenthesized path call")
47 |+Path(
48 |+ (("parenthesized path call")
51 49 | # Comment between closing parentheses
50 |+),
52 51 | )
53 52 |
54 53 | Path(
PTH201.py:55:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
54 | Path(
55 | '' # Comment before comma
| ^^ PTH201
56 | ,
57 | ) / "multiple" / (
|
= help: Remove the current directory argument
Unsafe fix
52 52 | )
53 53 |
54 54 | Path(
55 |- '' # Comment before comma
55 |+ "multiple" # Comment before comma
56 56 | ,
57 |-) / "multiple" / (
57 |+) / (
58 58 | "frag" # Comment
59 59 | 'ment'
60 60 | )
PTH201.py:74:15: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
72 | from importlib.metadata import PackagePath
73 |
74 | _ = PosixPath(".")
| ^^^ PTH201
75 | _ = PurePosixPath(".")
76 | _ = WindowsPath(".")
|
= help: Remove the current directory argument
Safe fix
71 71 |
72 72 | from importlib.metadata import PackagePath
73 73 |
74 |-_ = PosixPath(".")
74 |+_ = PosixPath()
75 75 | _ = PurePosixPath(".")
76 76 | _ = WindowsPath(".")
77 77 | _ = PureWindowsPath(".")
PTH201.py:75:19: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
74 | _ = PosixPath(".")
75 | _ = PurePosixPath(".")
| ^^^ PTH201
76 | _ = WindowsPath(".")
77 | _ = PureWindowsPath(".")
|
= help: Remove the current directory argument
Safe fix
72 72 | from importlib.metadata import PackagePath
73 73 |
74 74 | _ = PosixPath(".")
75 |-_ = PurePosixPath(".")
75 |+_ = PurePosixPath()
76 76 | _ = WindowsPath(".")
77 77 | _ = PureWindowsPath(".")
78 78 | _ = PackagePath(".")
PTH201.py:76:17: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
74 | _ = PosixPath(".")
75 | _ = PurePosixPath(".")
76 | _ = WindowsPath(".")
| ^^^ PTH201
77 | _ = PureWindowsPath(".")
78 | _ = PackagePath(".")
|
= help: Remove the current directory argument
Safe fix
73 73 |
74 74 | _ = PosixPath(".")
75 75 | _ = PurePosixPath(".")
76 |-_ = WindowsPath(".")
76 |+_ = WindowsPath()
77 77 | _ = PureWindowsPath(".")
78 78 | _ = PackagePath(".")
PTH201.py:77:21: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
75 | _ = PurePosixPath(".")
76 | _ = WindowsPath(".")
77 | _ = PureWindowsPath(".")
| ^^^ PTH201
78 | _ = PackagePath(".")
|
= help: Remove the current directory argument
Safe fix
74 74 | _ = PosixPath(".")
75 75 | _ = PurePosixPath(".")
76 76 | _ = WindowsPath(".")
77 |-_ = PureWindowsPath(".")
77 |+_ = PureWindowsPath()
78 78 | _ = PackagePath(".")
PTH201.py:78:17: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
76 | _ = WindowsPath(".")
77 | _ = PureWindowsPath(".")
78 | _ = PackagePath(".")
| ^^^ PTH201
|
= help: Remove the current directory argument
Safe fix
75 75 | _ = PurePosixPath(".")
76 76 | _ = WindowsPath(".")
77 77 | _ = PureWindowsPath(".")
78 |-_ = PackagePath(".")
78 |+_ = PackagePath()