[`flake8-use-pathlib`] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 (#19331)

Skips [invalid-pathlib-with-suffix
(PTH210)](https://docs.astral.sh/ruff/rules/invalid-pathlib-with-suffix/#invalid-pathlib-with-suffix-pth210)
for `.with_suffix(".")` on Python versions 3.14 and greater, as per [the
docs](https://docs.python.org/3.14/library/pathlib.html#pathlib.PurePath.with_suffix).

Progress towards #15506
This commit is contained in:
Dylan 2025-07-15 07:05:00 -05:00 committed by GitHub
parent 464144f1c6
commit 82391b5675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 60 additions and 6 deletions

View File

@ -0,0 +1,26 @@
from pathlib import (
Path,
PosixPath,
PurePath,
PurePosixPath,
PureWindowsPath,
WindowsPath,
)
import pathlib
path = Path()
posix_path: pathlib.PosixPath = PosixPath()
pure_path: PurePath = PurePath()
pure_posix_path = pathlib.PurePosixPath()
pure_windows_path: PureWindowsPath = pathlib.PureWindowsPath()
windows_path: pathlib.WindowsPath = pathlib.WindowsPath()
### No Errors
path.with_suffix(".")
posix_path.with_suffix(".")
pure_path.with_suffix(".")
pure_posix_path.with_suffix(".")
pure_windows_path.with_suffix(".")
windows_path.with_suffix(".")

View File

@ -8,6 +8,7 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use ruff_python_ast::PythonVersion;
use test_case::test_case;
use crate::assert_diagnostics;
@ -143,4 +144,22 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::InvalidPathlibWithSuffix, Path::new("PTH210_2.py"))]
fn pathlib_with_suffix_py314(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"py314__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_use_pathlib").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY314.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@ -1,7 +1,7 @@
use crate::checkers::ast::Checker;
use crate::{Edit, Fix, FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, StringFlags};
use ruff_python_ast::{self as ast, PythonVersion, StringFlags};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
@ -9,12 +9,13 @@ use ruff_text_size::Ranged;
/// ## What it does
/// Checks for `pathlib.Path.with_suffix()` calls where
/// the given suffix does not have a leading dot
/// or the given suffix is a single dot `"."`.
/// or the given suffix is a single dot `"."` and the
/// Python version is less than 3.14.
///
/// ## Why is this bad?
/// `Path.with_suffix()` will raise an error at runtime
/// if the given suffix is not prefixed with a dot
/// or it is a single dot `"."`.
/// or, in versions prior to Python 3.14, if it is a single dot `"."`.
///
/// ## Example
///
@ -57,9 +58,6 @@ use ruff_text_size::Ranged;
/// No fix is offered if the suffix `"."` is given, since the intent is unclear.
#[derive(ViolationMetadata)]
pub(crate) struct InvalidPathlibWithSuffix {
// TODO: Since "." is a correct suffix in Python 3.14,
// we will need to update this rule and documentation
// once Ruff supports Python 3.14.
single_dot: bool,
}
@ -116,6 +114,13 @@ pub(crate) fn invalid_pathlib_with_suffix(checker: &Checker, call: &ast::ExprCal
};
let single_dot = string_value == ".";
// As of Python 3.14, a single dot is considered a valid suffix.
// https://docs.python.org/3.14/library/pathlib.html#pathlib.PurePath.with_suffix
if single_dot && checker.target_version() >= PythonVersion::PY314 {
return;
}
let mut diagnostic =
checker.report_diagnostic(InvalidPathlibWithSuffix { single_dot }, call.range);
if !single_dot {

View File

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---