Compare commits

..

2 Commits

Author SHA1 Message Date
Aria Desires
7ef86c9637 fixup 2025-09-18 21:08:49 -04:00
Aria Desires
793d0d0dd4 Implement additional implicit re-exports idiom for __init__.pyi 2025-09-18 15:09:01 -04:00
53 changed files with 459 additions and 1185 deletions

View File

@@ -1,65 +1,5 @@
# Changelog
## 0.13.1
Released on 2025-09-18.
### Preview features
- \[`flake8-simplify`\] Detect unnecessary `None` default for additional key expression types (`SIM910`) ([#20343](https://github.com/astral-sh/ruff/pull/20343))
- \[`flake8-use-pathlib`\] Add fix for `PTH123` ([#20169](https://github.com/astral-sh/ruff/pull/20169))
- \[`flake8-use-pathlib`\] Fix `PTH101`, `PTH104`, `PTH105`, `PTH121` fixes ([#20143](https://github.com/astral-sh/ruff/pull/20143))
- \[`flake8-use-pathlib`\] Make `PTH111` fix unsafe because it can change behavior ([#20215](https://github.com/astral-sh/ruff/pull/20215))
- \[`pycodestyle`\] Fix `E301` to only trigger for functions immediately within a class ([#19768](https://github.com/astral-sh/ruff/pull/19768))
- \[`refurb`\] Mark `single-item-membership-test` fix as always unsafe (`FURB171`) ([#20279](https://github.com/astral-sh/ruff/pull/20279))
### Bug fixes
- Handle t-strings for token-based rules and suppression comments ([#20357](https://github.com/astral-sh/ruff/pull/20357))
- \[`flake8-bandit`\] Fix truthiness: dict-only `**` displays not truthy for `shell` (`S602`, `S604`, `S609`) ([#20177](https://github.com/astral-sh/ruff/pull/20177))
- \[`flake8-simplify`\] Fix diagnostic to show correct method name for `str.rsplit` calls (`SIM905`) ([#20459](https://github.com/astral-sh/ruff/pull/20459))
- \[`flynt`\] Use triple quotes for joined raw strings with newlines (`FLY002`) ([#20197](https://github.com/astral-sh/ruff/pull/20197))
- \[`pyupgrade`\] Fix false positive when class name is shadowed by local variable (`UP008`) ([#20427](https://github.com/astral-sh/ruff/pull/20427))
- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20327](https://github.com/astral-sh/ruff/pull/20327))
- \[`ruff`\] Recognize t-strings, generators, and lambdas in `invalid-index-type` (`RUF016`) ([#20213](https://github.com/astral-sh/ruff/pull/20213))
### Rule changes
- \[`RUF102`\] Respect rule redirects in invalid rule code detection ([#20245](https://github.com/astral-sh/ruff/pull/20245))
- \[`flake8-bugbear`\] Mark the fix for `unreliable-callable-check` as always unsafe (`B004`) ([#20318](https://github.com/astral-sh/ruff/pull/20318))
- \[`ruff`\] Allow dataclass attribute value instantiation from nested frozen dataclass (`RUF009`) ([#20352](https://github.com/astral-sh/ruff/pull/20352))
### CLI
- Add fixes to `output-format=sarif` ([#20300](https://github.com/astral-sh/ruff/pull/20300))
- Treat panics as fatal diagnostics, sort panics last ([#20258](https://github.com/astral-sh/ruff/pull/20258))
### Documentation
- \[`ruff`\] Add `analyze.string-imports-min-dots` to settings ([#20375](https://github.com/astral-sh/ruff/pull/20375))
- Update README.md with Albumentations new repository URL ([#20415](https://github.com/astral-sh/ruff/pull/20415))
### Other changes
- Bump MSRV to Rust 1.88 ([#20470](https://github.com/astral-sh/ruff/pull/20470))
- Enable inline noqa for multiline strings in playground ([#20442](https://github.com/astral-sh/ruff/pull/20442))
### Contributors
- [@chirizxc](https://github.com/chirizxc)
- [@danparizher](https://github.com/danparizher)
- [@IDrokin117](https://github.com/IDrokin117)
- [@amyreese](https://github.com/amyreese)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@dylwil3](https://github.com/dylwil3)
- [@njhearp](https://github.com/njhearp)
- [@woodruffw](https://github.com/woodruffw)
- [@dcreager](https://github.com/dcreager)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@BurntSushi](https://github.com/BurntSushi)
- [@salahelfarissi](https://github.com/salahelfarissi)
- [@MichaReiser](https://github.com/MichaReiser)
## 0.13.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration

6
Cargo.lock generated
View File

@@ -2728,7 +2728,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.13.1"
version = "0.13.0"
dependencies = [
"anyhow",
"argfile",
@@ -2984,7 +2984,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.13.1"
version = "0.13.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3338,7 +3338,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.13.1"
version = "0.13.0"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
rust-version = "1.88"
rust-version = "1.87"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.13.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.13.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.13.1"
version = "0.13.0"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -22,30 +22,6 @@ exit_code: 1
{
"results": [
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[TMP]/input.py"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 1,
"endLine": 2,
"startColumn": 1,
"startLine": 1
}
}
]
}
],
"description": {
"text": "Remove unused import: `os`"
}
}
],
"level": "error",
"locations": [
{

View File

@@ -58,11 +58,10 @@ impl<'a> FullRenderer<'a> {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
}
if self.config.show_fix_diff
&& diag.has_applicable_fix(self.config)
&& let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver)
{
write!(f, "{diff}")?;
if self.config.show_fix_diff && diag.has_applicable_fix(self.config) {
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
write!(f, "{diff}")?;
}
}
writeln!(f)?;

View File

@@ -459,6 +459,12 @@ impl File {
self.source_type(db).is_stub()
}
/// Returns `true` if the file is an `__init__.py(i)`
pub fn is_init(self, db: &dyn Db) -> bool {
let path = self.path(db).as_str();
path.ends_with("__init__.py") || path.ends_with("__init__.pyi")
}
pub fn source_type(self, db: &dyn Db) -> PySourceType {
match self.path(db) {
FilePath::System(path) => path

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.13.1"
version = "0.13.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -125,15 +125,3 @@ os.makedirs("name", 0o777, False)
os.makedirs(name="name", mode=0o777, exist_ok=False)
os.makedirs("name", unknown_kwarg=True)
# https://github.com/astral-sh/ruff/issues/20134
os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
# Only diagnostic
os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)

View File

@@ -16,9 +16,7 @@ nok4 = "a".join([a, a, *a]) # Not OK (not a static length)
nok5 = "a".join([choice("flarp")]) # Not OK (not a simple call)
nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
# https://github.com/astral-sh/ruff/issues/19887
nok8 = '\n'.join([r'line1','line2'])
nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
# Regression test for: https://github.com/astral-sh/ruff/issues/7197
def create_file_public_url(url, filename):

View File

@@ -271,19 +271,3 @@ class ChildI9(ParentI):
if False: super
if False: __class__
builtins.super(ChildI9, self).f()
# See: https://github.com/astral-sh/ruff/issues/20422
# UP008 should not apply when the class variable is shadowed
class A:
def f(self):
return 1
class B(A):
def f(self):
return 2
class C(B):
def f(self):
C = B # Local variable C shadows the class name
return super(C, self).f() # Should NOT trigger UP008

View File

@@ -117,31 +117,3 @@ def _():
# After
] and \
0 < 1: ...
# https://github.com/astral-sh/ruff/issues/20255
import math
# NaN behavior differences
if math.nan in [math.nan]:
print("This is True")
if math.nan in (math.nan,):
print("This is True")
if math.nan in {math.nan}:
print("This is True")
# Potential type differences with custom __eq__ methods
class CustomEq:
def __eq__(self, other):
return "custom"
obj = CustomEq()
if obj in [CustomEq()]:
pass
if obj in (CustomEq(),):
pass
if obj in {CustomEq()}:
pass

View File

@@ -51,13 +51,3 @@ if 1 in set(1,2):
if 1 in set((x for x in range(2))):
pass
# https://github.com/astral-sh/ruff/issues/20255
import math
# set() and frozenset() with NaN
if math.nan in set([math.nan]):
print("This should be marked unsafe")
if math.nan in frozenset([math.nan]):
print("This should be marked unsafe")

View File

@@ -2,24 +2,17 @@ use std::collections::HashSet;
use std::io::Write;
use anyhow::Result;
use log::warn;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
use ruff_source_file::{OneIndexed, SourceFile};
use ruff_text_size::{Ranged, TextRange};
use ruff_source_file::OneIndexed;
use crate::VERSION;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext};
use crate::registry::{Linter, RuleNamespace};
/// An emitter for producing SARIF 2.1.0-compliant JSON output.
///
/// Static Analysis Results Interchange Format (SARIF) is a standard format
/// for static analysis results. For full specfification, see:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
pub struct SarifEmitter;
impl Emitter for SarifEmitter {
@@ -36,7 +29,7 @@ impl Emitter for SarifEmitter {
let unique_rules: HashSet<_> = results
.iter()
.filter_map(|result| result.rule_id.as_secondary_code())
.filter_map(|result| result.code.as_secondary_code())
.collect();
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
rules.sort_by(|a, b| a.code.cmp(b.code));
@@ -141,15 +134,6 @@ impl RuleCode<'_> {
}
}
impl Serialize for RuleCode<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
fn from(code: &'a Diagnostic) -> Self {
match code.secondary_code() {
@@ -159,83 +143,12 @@ impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
}
}
/// Represents a single result in a SARIF 2.1.0 report.
///
/// See the SARIF 2.1.0 specification for details:
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug)]
struct SarifResult<'a> {
rule_id: RuleCode<'a>,
code: RuleCode<'a>,
level: String,
message: SarifMessage,
locations: Vec<SarifLocation>,
#[serde(skip_serializing_if = "Vec::is_empty")]
fixes: Vec<SarifFix>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifMessage {
text: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifPhysicalLocation {
artifact_location: SarifArtifactLocation,
region: SarifRegion,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifLocation {
physical_location: SarifPhysicalLocation,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifFix {
description: RuleDescription,
artifact_changes: Vec<SarifArtifactChange>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RuleDescription {
text: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactChange {
artifact_location: SarifArtifactLocation,
replacements: Vec<SarifReplacement>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifArtifactLocation {
message: String,
uri: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SarifReplacement {
deleted_region: SarifRegion,
#[serde(skip_serializing_if = "Option::is_none")]
inserted_content: Option<InsertedContent>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct InsertedContent {
text: String,
}
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "camelCase")]
struct SarifRegion {
start_line: OneIndexed,
start_column: OneIndexed,
end_line: OneIndexed,
@@ -243,108 +156,71 @@ struct SarifRegion {
}
impl<'a> SarifResult<'a> {
fn range_to_sarif_region(source_file: &SourceFile, range: TextRange) -> SarifRegion {
let source_code = source_file.to_source_code();
let start_location = source_code.line_column(range.start());
let end_location = source_code.line_column(range.end());
SarifRegion {
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
}
}
fn fix(diagnostic: &'a Diagnostic, uri: &str) -> Option<SarifFix> {
let fix = diagnostic.fix()?;
let Some(source_file) = diagnostic.ruff_source_file() else {
debug_assert!(
false,
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
diagnostic.id()
);
warn!(
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
diagnostic.id()
);
return None;
};
let fix_description = diagnostic
.first_help_text()
.map(std::string::ToString::to_string);
let replacements: Vec<SarifReplacement> = fix
.edits()
.iter()
.map(|edit| {
let range = edit.range();
let deleted_region = Self::range_to_sarif_region(source_file, range);
SarifReplacement {
deleted_region,
inserted_content: edit.content().map(|content| InsertedContent {
text: content.to_string(),
}),
}
})
.collect();
let artifact_changes = vec![SarifArtifactChange {
artifact_location: SarifArtifactLocation {
uri: uri.to_string(),
},
replacements,
}];
Some(SarifFix {
description: RuleDescription {
text: fix_description,
},
artifact_changes,
})
}
#[allow(clippy::unnecessary_wraps)]
fn uri(diagnostic: &Diagnostic) -> Result<String> {
let path = normalize_path(&*diagnostic.expect_ruff_filename());
#[cfg(not(target_arch = "wasm32"))]
return url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))
.map(|u| u.to_string());
#[cfg(target_arch = "wasm32")]
return Ok(format!("file://{}", path.display()));
}
fn from_message(diagnostic: &'a Diagnostic) -> Result<Self> {
let start_location = diagnostic.ruff_start_location().unwrap_or_default();
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
let region = SarifRegion {
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
};
let uri = Self::uri(diagnostic)?;
#[cfg(not(target_arch = "wasm32"))]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
rule_id: RuleCode::from(diagnostic),
code: RuleCode::from(message),
level: "error".to_string(),
message: SarifMessage {
text: diagnostic.body().to_string(),
},
fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
locations: vec![SarifLocation {
physical_location: SarifPhysicalLocation {
artifact_location: SarifArtifactLocation { uri },
region,
},
}],
message: message.body().to_string(),
uri: url::Url::from_file_path(&path)
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
.to_string(),
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
})
}
#[cfg(target_arch = "wasm32")]
#[expect(clippy::unnecessary_wraps)]
fn from_message(message: &'a Diagnostic) -> Result<Self> {
let start_location = message.ruff_start_location().unwrap_or_default();
let end_location = message.ruff_end_location().unwrap_or_default();
let path = normalize_path(&*message.expect_ruff_filename());
Ok(Self {
code: RuleCode::from(message),
level: "error".to_string(),
message: message.body().to_string(),
uri: path.display().to_string(),
start_line: start_location.line,
start_column: start_location.column,
end_line: end_location.line,
end_column: end_location.column,
})
}
}
impl Serialize for SarifResult<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
json!({
"level": self.level,
"message": {
"text": self.message,
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": self.uri,
},
"region": {
"startLine": self.start_line,
"startColumn": self.start_column,
"endLine": self.end_line,
"endColumn": self.end_column,
}
}
}],
"ruleId": self.code.as_str(),
})
.serialize(serializer)
}
}
#[cfg(test)]
@@ -380,7 +256,6 @@ mod tests {
insta::assert_json_snapshot!(value, {
".runs[0].tool.driver.version" => "[VERSION]",
".runs[0].results[].locations[].physicalLocation.artifactLocation.uri" => "[URI]",
".runs[0].results[].fixes[].artifactChanges[].artifactLocation.uri" => "[URI]",
});
}
}

View File

@@ -8,30 +8,6 @@ expression: value
{
"results": [
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[URI]"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 1,
"endLine": 2,
"startColumn": 1,
"startLine": 1
}
}
]
}
],
"description": {
"text": "Remove unused import: `os`"
}
}
],
"level": "error",
"locations": [
{
@@ -54,30 +30,6 @@ expression: value
"ruleId": "F401"
},
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "[URI]"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 10,
"endLine": 6,
"startColumn": 5,
"startLine": 6
}
}
]
}
],
"description": {
"text": "Remove assignment to unused variable `x`"
}
}
],
"level": "error",
"locations": [
{

View File

@@ -487,6 +487,7 @@ impl<'a> Iterator for PathParamIterator<'a> {
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
let param_name = &param_content[..param_name_end];
#[expect(clippy::range_plus_one)]
return Some((param_name, start..end + 1));
}
}

View File

@@ -1,3 +1,5 @@
use std::cmp::Ordering;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StringFlags;
use ruff_python_ast::{
@@ -5,8 +7,6 @@ use ruff_python_ast::{
StringLiteralValue, UnaryOp, str::TripleQuotes,
};
use ruff_text_size::{Ranged, TextRange};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use crate::checkers::ast::Checker;
use crate::preview::is_maxsplit_without_separator_fix_enabled;
@@ -47,40 +47,14 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: `str.split`](https://docs.python.org/3/library/stdtypes.html#str.split)
#[derive(ViolationMetadata)]
pub(crate) struct SplitStaticString {
method: Method,
}
#[derive(Copy, Clone, Debug)]
enum Method {
Split,
RSplit,
}
impl Method {
fn is_split(self) -> bool {
matches!(self, Method::Split)
}
}
impl Display for Method {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Method::Split => f.write_str("split"),
Method::RSplit => f.write_str("rsplit"),
}
}
}
pub(crate) struct SplitStaticString;
impl Violation for SplitStaticString {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Consider using a list literal instead of `str.{}`",
self.method
)
"Consider using a list literal instead of `str.split`".to_string()
}
fn fix_title(&self) -> Option<String> {
@@ -103,21 +77,26 @@ pub(crate) fn split_static_string(
};
// `split` vs `rsplit`.
let method = if attr == "split" {
Method::Split
let direction = if attr == "split" {
Direction::Left
} else {
Method::RSplit
Direction::Right
};
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => {
split_default(str_value, maxsplit_value, method, checker.settings())
split_default(str_value, maxsplit_value, direction, checker.settings())
}
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(str_value, sep_value_str, maxsplit_value, method))
Some(split_sep(
str_value,
sep_value_str,
maxsplit_value,
direction,
))
}
// Ignore names until type inference is available.
_ => {
@@ -125,10 +104,10 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value, method, checker.settings())
split_default(str_value, maxsplit_value, direction, checker.settings())
};
let mut diagnostic = checker.report_diagnostic(SplitStaticString { method }, call.range());
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
if let Some(ref replacement_expr) = split_replacement {
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(checker.generator().expr(replacement_expr), call.range()),
@@ -198,7 +177,7 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
method: Method,
direction: Direction,
settings: &LinterSettings,
) -> Option<Expr> {
// From the Python documentation:
@@ -217,7 +196,7 @@ fn split_default(
let Ok(max_split) = usize::try_from(max_split) else {
return None;
};
let list_items: Vec<&str> = if method.is_split() {
let list_items: Vec<&str> = if direction == Direction::Left {
string_val
.trim_start_matches(py_unicode_is_whitespace)
.splitn(max_split + 1, py_unicode_is_whitespace)
@@ -245,7 +224,7 @@ fn split_default(
// - " x ".rsplit(maxsplit=0) -> [' x']
// - "".split(maxsplit=0) -> []
// - " ".split(maxsplit=0) -> []
let processed_str = if method.is_split() {
let processed_str = if direction == Direction::Left {
string_val.trim_start_matches(py_unicode_is_whitespace)
} else {
string_val.trim_end_matches(py_unicode_is_whitespace)
@@ -277,22 +256,22 @@ fn split_sep(
str_value: &StringLiteralValue,
sep_value: &str,
max_split: i32,
method: Method,
direction: Direction,
) -> Expr {
let value = str_value.to_str();
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
match method {
Method::Split => value.splitn(split_n + 1, sep_value).collect(),
Method::RSplit => {
match direction {
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
items.reverse();
items
}
}
} else {
match method {
Method::Split => value.split(sep_value).collect(),
Method::RSplit => {
match direction {
Direction::Left => value.split(sep_value).collect(),
Direction::Right => {
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
items.reverse();
items
@@ -337,6 +316,12 @@ fn get_maxsplit_value(arg: Option<&Expr>) -> Option<i32> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Direction {
Left,
Right,
}
/// Like [`char::is_whitespace`] but with Python's notion of whitespace.
///
/// <https://github.com/astral-sh/ruff/issues/19845>

View File

@@ -846,7 +846,7 @@ help: Replace with list literal
105 | # https://github.com/astral-sh/ruff/issues/18042
106 | print("a,b".rsplit(","))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:111:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -864,7 +864,7 @@ help: Replace with list literal
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:112:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -1043,7 +1043,7 @@ help: Replace with list literal
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:124:7
|
122 | print(" x ".split(maxsplit=0))
@@ -1063,7 +1063,7 @@ help: Replace with list literal
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:125:7
|
123 | print(" x ".split(sep=None, maxsplit=0))
@@ -1083,7 +1083,7 @@ help: Replace with list literal
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:126:7
|
124 | print("".rsplit(maxsplit=0))
@@ -1103,7 +1103,7 @@ help: Replace with list literal
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:127:7
|
125 | print("".rsplit(sep=None, maxsplit=0))
@@ -1123,7 +1123,7 @@ help: Replace with list literal
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:128:7
|
126 | print(" ".rsplit(maxsplit=0))
@@ -1143,7 +1143,7 @@ help: Replace with list literal
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:129:7
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
@@ -1163,7 +1163,7 @@ help: Replace with list literal
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:130:7
|
128 | print(" x ".rsplit(maxsplit=0))
@@ -1183,7 +1183,7 @@ help: Replace with list literal
132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 |
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:131:7
|
129 | print(" x ".rsplit(maxsplit=0))
@@ -1202,7 +1202,7 @@ help: Replace with list literal
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:132:7
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
@@ -1345,7 +1345,7 @@ help: Replace with list literal
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:168:7
|
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
@@ -1375,7 +1375,7 @@ SIM905 Consider using a list literal instead of `str.split`
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.rsplit`
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit

View File

@@ -894,7 +894,7 @@ help: Replace with list literal
105 | # https://github.com/astral-sh/ruff/issues/18042
106 | print("a,b".rsplit(","))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:111:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -912,7 +912,7 @@ help: Replace with list literal
113 |
114 | # https://github.com/astral-sh/ruff/issues/18069
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:112:7
|
110 | # https://github.com/astral-sh/ruff/issues/18042
@@ -1091,7 +1091,7 @@ help: Replace with list literal
125 | print("".rsplit(sep=None, maxsplit=0))
126 | print(" ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:124:7
|
122 | print(" x ".split(maxsplit=0))
@@ -1111,7 +1111,7 @@ help: Replace with list literal
126 | print(" ".rsplit(maxsplit=0))
127 | print(" ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:125:7
|
123 | print(" x ".split(sep=None, maxsplit=0))
@@ -1131,7 +1131,7 @@ help: Replace with list literal
127 | print(" ".rsplit(sep=None, maxsplit=0))
128 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:126:7
|
124 | print("".rsplit(maxsplit=0))
@@ -1151,7 +1151,7 @@ help: Replace with list literal
128 | print(" x ".rsplit(maxsplit=0))
129 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:127:7
|
125 | print("".rsplit(sep=None, maxsplit=0))
@@ -1171,7 +1171,7 @@ help: Replace with list literal
129 | print(" x ".rsplit(maxsplit=0))
130 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:128:7
|
126 | print(" ".rsplit(maxsplit=0))
@@ -1191,7 +1191,7 @@ help: Replace with list literal
130 | print(" x ".rsplit(sep=None, maxsplit=0))
131 | print(" x ".rsplit(maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:129:7
|
127 | print(" ".rsplit(sep=None, maxsplit=0))
@@ -1211,7 +1211,7 @@ help: Replace with list literal
131 | print(" x ".rsplit(maxsplit=0))
132 | print(" x ".rsplit(sep=None, maxsplit=0))
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:130:7
|
128 | print(" x ".rsplit(maxsplit=0))
@@ -1231,7 +1231,7 @@ help: Replace with list literal
132 | print(" x ".rsplit(sep=None, maxsplit=0))
133 |
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:131:7
|
129 | print(" x ".rsplit(maxsplit=0))
@@ -1250,7 +1250,7 @@ help: Replace with list literal
133 |
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:132:7
|
130 | print(" x ".rsplit(sep=None, maxsplit=0))
@@ -1393,7 +1393,7 @@ help: Replace with list literal
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:168:7
|
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
@@ -1429,7 +1429,7 @@ help: Replace with list literal
171 + ["a", "b", "c d "] # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
SIM905 [*] Consider using a list literal instead of `str.rsplit`
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged;
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
pub(crate) fn is_keyword_only_argument_non_default(arguments: &Arguments, name: &str) -> bool {
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
@@ -24,7 +24,10 @@ 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: &Checker, segments: &[&str]) -> bool {
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 {
@@ -190,7 +193,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
}
pub(crate) fn has_unknown_keywords_or_starred_expr(
arguments: &Arguments,
arguments: &ast::Arguments,
allowed: &[&str],
) -> bool {
if arguments.args.iter().any(Expr::is_starred_expr) {
@@ -204,7 +207,11 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, position: usize) -> bool {
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())

View File

@@ -1,16 +1,11 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ArgOrKeyword, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_chmod_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_file_descriptor, is_keyword_only_argument_non_default,
is_pathlib_path_call,
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`.
@@ -78,80 +73,22 @@ pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// 0 1 2 3
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
// ```
let path_arg = call.arguments.find_argument_value("path", 0);
if path_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
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;
}
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsChmod, call.func.range());
if !is_fix_os_chmod_enabled(checker.settings()) {
return;
}
if call.arguments.len() < 2 {
return;
}
if has_unknown_keywords_or_starred_expr(
&call.arguments,
&["path", "mode", "dir_fd", "follow_symlinks"],
) {
return;
}
let (Some(path_arg), Some(_)) = (path_arg, call.arguments.find_argument_value("mode", 1))
else {
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 path_code = locator.slice(path_arg.range());
let args = |arg: ArgOrKeyword| match arg {
ArgOrKeyword::Arg(expr) if expr.range() != path_arg.range() => {
Some(locator.slice(expr.range()))
}
ArgOrKeyword::Keyword(kw)
if matches!(kw.arg.as_deref(), Some("mode" | "follow_symlinks")) =>
{
Some(locator.slice(kw.range()))
}
_ => None,
};
let chmod_args = itertools::join(
call.arguments.arguments_source_order().filter_map(args),
", ",
);
let replacement = if is_pathlib_path_call(checker, path_arg) {
format!("{path_code}.chmod({chmod_args})")
} else {
format!("{binding}({path_code}).chmod({chmod_args})")
};
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,
))
});
check_os_pathlib_two_arg_calls(
checker,
call,
"chmod",
"path",
"mode",
is_fix_os_chmod_enabled(checker.settings()),
OsChmod,
);
}

View File

@@ -1,8 +1,6 @@
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, has_unknown_keywords_or_starred_expr,
};
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;
@@ -67,16 +65,13 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
return;
}
let fix_enabled = is_fix_os_path_samefile_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(&call.arguments, &["f1", "f2"]);
check_os_pathlib_two_arg_calls(
checker,
call,
"samefile",
"f1",
"f2",
fix_enabled,
is_fix_os_path_samefile_enabled(checker.settings()),
OsPathSamefile,
);
}

View File

@@ -1,8 +1,7 @@
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, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -80,11 +79,13 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
return;
}
let fix_enabled = is_fix_os_rename_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
check_os_pathlib_two_arg_calls(
checker,
call,
"rename",
"src",
"dst",
is_fix_os_rename_enabled(checker.settings()),
OsRename,
);
}

View File

@@ -1,8 +1,7 @@
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, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default,
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
};
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
@@ -83,19 +82,13 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
}
let fix_enabled = is_fix_os_replace_enabled(checker.settings())
&& !has_unknown_keywords_or_starred_expr(
&call.arguments,
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
);
check_os_pathlib_two_arg_calls(
checker,
call,
"replace",
"src",
"dst",
fix_enabled,
is_fix_os_replace_enabled(checker.settings()),
OsReplace,
);
}

View File

@@ -104,6 +104,14 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
};
let target_is_directory_arg = call.arguments.find_argument_value("target_is_directory", 2);
if let Some(expr) = &target_is_directory_arg {
if expr.as_boolean_literal_expr().is_none() {
return;
}
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
@@ -121,9 +129,7 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
let src_code = locator.slice(src.range());
let dst_code = locator.slice(dst.range());
let target_is_directory = call
.arguments
.find_argument_value("target_is_directory", 2)
let target_is_directory = target_is_directory_arg
.and_then(|expr| {
let code = locator.slice(expr.range());
expr.as_boolean_literal_expr()

View File

@@ -500,72 +500,5 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
128 |
129 | # https://github.com/astral-sh/ruff/issues/20134
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:130:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
| ^^^^^^^^
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
help: Replace with `Path(...).chmod(...)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:131:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
| ^^^^^^^^
132 |
133 | # Only diagnostic
|
help: Replace with `Path(...).chmod(...)`
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:134:1
|
133 | # Only diagnostic
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).chmod(...)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:136:1
|
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:137:1
|
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).replace(...)`
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:139:1
|
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
|
help: Replace with `Path(...).samefile()`

View File

@@ -931,7 +931,6 @@ help: Replace with `Path(...).mkdir(parents=True)`
126 + pathlib.Path("name").mkdir(mode=0o777, exist_ok=False, parents=True)
127 |
128 | os.makedirs("name", unknown_kwarg=True)
129 |
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
--> full_name.py:127:1
@@ -940,102 +939,5 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
126 |
127 | os.makedirs("name", unknown_kwarg=True)
| ^^^^^^^^^^^
128 |
129 | # https://github.com/astral-sh/ruff/issues/20134
|
help: Replace with `Path(...).mkdir(parents=True)`
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:130:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
| ^^^^^^^^
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
help: Replace with `Path(...).chmod(...)`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
128 | os.makedirs("name", unknown_kwarg=True)
129 |
130 | # https://github.com/astral-sh/ruff/issues/20134
- os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks= False)
132 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
133 |
134 | # Only diagnostic
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:131:1
|
129 | # https://github.com/astral-sh/ruff/issues/20134
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
| ^^^^^^^^
132 |
133 | # Only diagnostic
|
help: Replace with `Path(...).chmod(...)`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
129 |
130 | # https://github.com/astral-sh/ruff/issues/20134
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
- os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
132 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks=True)
133 |
134 | # Only diagnostic
135 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
--> full_name.py:134:1
|
133 | # Only diagnostic
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).chmod(...)`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:136:1
|
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
135 |
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:137:1
|
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
help: Replace with `Path(...).replace(...)`
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
--> full_name.py:139:1
|
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
|
help: Replace with `Path(...).samefile()`

View File

@@ -2,7 +2,7 @@ use ast::FStringFlags;
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -72,42 +72,24 @@ fn is_static_length(elts: &[Expr]) -> bool {
fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<Expr> {
// If all elements are string constants, join them into a single string.
if joinees.iter().all(Expr::is_string_literal_expr) {
let mut flags: Option<ast::StringLiteralFlags> = None;
let content = joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// Take the flags from the first Expr
flags = Some(value.first_literal_flags());
}
Some(value.to_str())
} else {
None
}
})
.join(joiner);
let mut flags = flags?;
// If the result is a raw string and contains a newline, use triple quotes.
if flags.prefix().is_raw() && content.contains(['\n', '\r']) {
flags = flags.with_triple_quotes(ruff_python_ast::str::TripleQuotes::Yes);
// Prefer a delimiter that doesn't occur in the content; if both occur, bail.
if content.contains(flags.quote_str()) {
flags = flags.with_quote_style(flags.quote_style().opposite());
if content.contains(flags.quote_str()) {
// Both "'''" and "\"\"\"" are present in content; avoid emitting
// an invalid raw triple-quoted literal (or escaping). Bail on the fix.
return None;
}
}
}
let mut flags = None;
let node = ast::StringLiteral {
value: content.into_boxed_str(),
flags,
value: joinees
.iter()
.filter_map(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
if flags.is_none() {
// take the flags from the first Expr
flags = Some(value.first_literal_flags());
}
Some(value.to_str())
} else {
None
}
})
.join(joiner)
.into_boxed_str(),
flags: flags?,
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
};

View File

@@ -125,39 +125,18 @@ help: Replace with `f"{secrets.token_urlsafe()}a{secrets.token_hex()}"`
13 | nok2 = a.join(["1", "2", "3"]) # Not OK (not a static joiner)
note: This is an unsafe fix and may change runtime behavior
FLY002 [*] Consider f-string instead of string join
--> FLY002.py:20:8
|
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
19 | # https://github.com/astral-sh/ruff/issues/19887
20 | nok8 = '\n'.join([r'line1','line2'])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
|
help: Replace with f-string
17 | nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
19 | # https://github.com/astral-sh/ruff/issues/19887
- nok8 = '\n'.join([r'line1','line2'])
20 + nok8 = r'''line1
21 + line2'''
22 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
23 |
24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
note: This is an unsafe fix and may change runtime behavior
FLY002 [*] Consider `f"{url}{filename}"` instead of string join
--> FLY002.py:25:11
--> FLY002.py:23:11
|
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
24 | def create_file_public_url(url, filename):
25 | return''.join([url, filename])
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
22 | def create_file_public_url(url, filename):
23 | return''.join([url, filename])
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `f"{url}{filename}"`
22 |
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
24 | def create_file_public_url(url, filename):
20 |
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
22 | def create_file_public_url(url, filename):
- return''.join([url, filename])
25 + return f"{url}{filename}"
23 + return f"{url}{filename}"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -269,7 +269,7 @@ pub(crate) fn indentation(
range: TextRange,
context: &LintContext,
) {
if !indent_level.is_multiple_of(indent_size) {
if indent_level % indent_size != 0 {
if logical_line.is_comment_only() {
context.report_diagnostic_if_enabled(
IndentationWithInvalidMultipleComment {

View File

@@ -139,11 +139,7 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
return;
};
if !((first_arg_id == "__class__"
|| (first_arg_id == parent_name.as_str()
// If the first argument matches the class name, check if it's a local variable
// that shadows the class name. If so, don't apply UP008.
&& !checker.semantic().current_scope().has(first_arg_id)))
if !((first_arg_id == "__class__" || first_arg_id == parent_name.as_str())
&& second_arg_id == parent_arg.name().as_str())
{
return;

View File

@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for membership tests against single-item containers.
@@ -27,16 +27,13 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ```
///
/// ## Fix safety
/// The fix is always marked as unsafe.
///
/// When the right-hand side is a string, this fix can change the behavior of your program.
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string.
/// When the right-hand side is a string, the fix is marked as unsafe.
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string,
/// so the fix can change the behavior of your program in these cases.
///
/// Additionally, converting `in`/`not in` against a single-item container to `==`/`!=` can
/// change runtime behavior: `in` may consider identity (e.g., `NaN`) and always
/// yields a `bool`.
///
/// Comments within the replacement range will also be removed.
/// Additionally, if there are comments within the fix's range,
/// it will also be marked as unsafe.
///
/// ## References
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
@@ -103,8 +100,14 @@ pub(crate) fn single_item_membership_test(
expr.range(),
);
// All supported cases can change runtime behavior; mark as unsafe.
let fix = Fix::unsafe_edit(edit);
let applicability =
if right.is_string_literal_expr() || checker.comment_ranges().intersects(expr.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
};
let fix = Fix::applicable_edit(edit, applicability);
checker
.report_diagnostic(SingleItemMembershipTest { membership_test }, expr.range())

View File

@@ -18,7 +18,6 @@ help: Convert to equality test
4 | print("Single-element tuple")
5 |
6 | if 1 in [1]:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:6:4
@@ -38,7 +37,6 @@ help: Convert to equality test
7 | print("Single-element list")
8 |
9 | if 1 in {1}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:9:4
@@ -58,7 +56,6 @@ help: Convert to equality test
10 | print("Single-element set")
11 |
12 | if "a" in "a":
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:12:4
@@ -98,7 +95,6 @@ help: Convert to inequality test
16 | print("Check `not in` membership test")
17 |
18 | if not 1 in (1,):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:18:8
@@ -118,7 +114,6 @@ help: Convert to equality test
19 | print("Check the negated membership test")
20 |
21 | # Non-errors.
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:52:5
@@ -349,122 +344,4 @@ help: Convert to inequality test
- ] and \
113 + if foo != bar and \
114 | 0 < 1: ...
115 |
116 | # https://github.com/astral-sh/ruff/issues/20255
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:125:4
|
124 | # NaN behavior differences
125 | if math.nan in [math.nan]:
| ^^^^^^^^^^^^^^^^^^^^^^
126 | print("This is True")
|
help: Convert to equality test
122 | import math
123 |
124 | # NaN behavior differences
- if math.nan in [math.nan]:
125 + if math.nan == math.nan:
126 | print("This is True")
127 |
128 | if math.nan in (math.nan,):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:128:4
|
126 | print("This is True")
127 |
128 | if math.nan in (math.nan,):
| ^^^^^^^^^^^^^^^^^^^^^^^
129 | print("This is True")
|
help: Convert to equality test
125 | if math.nan in [math.nan]:
126 | print("This is True")
127 |
- if math.nan in (math.nan,):
128 + if math.nan == math.nan:
129 | print("This is True")
130 |
131 | if math.nan in {math.nan}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:131:4
|
129 | print("This is True")
130 |
131 | if math.nan in {math.nan}:
| ^^^^^^^^^^^^^^^^^^^^^^
132 | print("This is True")
|
help: Convert to equality test
128 | if math.nan in (math.nan,):
129 | print("This is True")
130 |
- if math.nan in {math.nan}:
131 + if math.nan == math.nan:
132 | print("This is True")
133 |
134 | # Potential type differences with custom __eq__ methods
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:140:4
|
139 | obj = CustomEq()
140 | if obj in [CustomEq()]:
| ^^^^^^^^^^^^^^^^^^^
141 | pass
|
help: Convert to equality test
137 | return "custom"
138 |
139 | obj = CustomEq()
- if obj in [CustomEq()]:
140 + if obj == CustomEq():
141 | pass
142 |
143 | if obj in (CustomEq(),):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:143:4
|
141 | pass
142 |
143 | if obj in (CustomEq(),):
| ^^^^^^^^^^^^^^^^^^^^
144 | pass
|
help: Convert to equality test
140 | if obj in [CustomEq()]:
141 | pass
142 |
- if obj in (CustomEq(),):
143 + if obj == CustomEq():
144 | pass
145 |
146 | if obj in {CustomEq()}:
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_0.py:146:4
|
144 | pass
145 |
146 | if obj in {CustomEq()}:
| ^^^^^^^^^^^^^^^^^^^
147 | pass
|
help: Convert to equality test
143 | if obj in (CustomEq(),):
144 | pass
145 |
- if obj in {CustomEq()}:
146 + if obj == CustomEq():
147 | pass
note: This is an unsafe fix and may change runtime behavior

View File

@@ -18,7 +18,6 @@ help: Convert to equality test
4 | print("Single-element set")
5 |
6 | if 1 in set((1,)):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:6:4
@@ -38,7 +37,6 @@ help: Convert to equality test
7 | print("Single-element set")
8 |
9 | if 1 in set({1}):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:9:4
@@ -58,7 +56,6 @@ help: Convert to equality test
10 | print("Single-element set")
11 |
12 | if 1 in frozenset([1]):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:12:4
@@ -78,7 +75,6 @@ help: Convert to equality test
13 | print("Single-element set")
14 |
15 | if 1 in frozenset((1,)):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:15:4
@@ -98,7 +94,6 @@ help: Convert to equality test
16 | print("Single-element set")
17 |
18 | if 1 in frozenset({1}):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:18:4
@@ -118,7 +113,6 @@ help: Convert to equality test
19 | print("Single-element set")
20 |
21 | if 1 in set(set([1])):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:21:4
@@ -137,42 +131,4 @@ help: Convert to equality test
21 + if 1 == 1:
22 | print('Recursive solution')
23 |
24 |
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:59:4
|
58 | # set() and frozenset() with NaN
59 | if math.nan in set([math.nan]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
60 | print("This should be marked unsafe")
|
help: Convert to equality test
56 | import math
57 |
58 | # set() and frozenset() with NaN
- if math.nan in set([math.nan]):
59 + if math.nan == math.nan:
60 | print("This should be marked unsafe")
61 |
62 | if math.nan in frozenset([math.nan]):
note: This is an unsafe fix and may change runtime behavior
FURB171 [*] Membership test against single-item container
--> FURB171_1.py:62:4
|
60 | print("This should be marked unsafe")
61 |
62 | if math.nan in frozenset([math.nan]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 | print("This should be marked unsafe")
|
help: Convert to equality test
59 | if math.nan in set([math.nan]):
60 | print("This should be marked unsafe")
61 |
- if math.nan in frozenset([math.nan]):
62 + if math.nan == math.nan:
63 | print("This should be marked unsafe")
note: This is an unsafe fix and may change runtime behavior
24 |

View File

@@ -3,7 +3,6 @@
use std::fmt::Write;
use std::ops::Deref;
use ruff_python_ast::str::Quote;
use ruff_python_ast::{
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator,
@@ -68,8 +67,6 @@ pub struct Generator<'a> {
indent: &'a Indentation,
/// The line ending to use.
line_ending: LineEnding,
/// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`].
preferred_quote: Option<Quote>,
buffer: String,
indent_depth: usize,
num_newlines: usize,
@@ -81,7 +78,6 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
Self {
indent: stylist.indentation(),
line_ending: stylist.line_ending(),
preferred_quote: None,
buffer: String::new(),
indent_depth: 0,
num_newlines: 0,
@@ -96,7 +92,6 @@ impl<'a> Generator<'a> {
// Style preferences.
indent,
line_ending,
preferred_quote: None,
// Internal state.
buffer: String::new(),
indent_depth: 0,
@@ -105,16 +100,6 @@ impl<'a> Generator<'a> {
}
}
/// Set a preferred quote style for generated source code.
///
/// - If [`None`], the generator will attempt to preserve the existing quote style whenever possible.
/// - If [`Some`], the generator will prefer the specified quote style, ignoring the one found in the source.
#[must_use]
pub fn with_preferred_quote(mut self, quote: Option<Quote>) -> Self {
self.preferred_quote = quote;
self
}
/// Generate source code from a [`Stmt`].
pub fn stmt(mut self, stmt: &Stmt) -> String {
self.unparse_stmt(stmt);
@@ -173,8 +158,7 @@ impl<'a> Generator<'a> {
return;
}
}
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
let escape = AsciiEscape::with_preferred_quote(s, quote_style);
let escape = AsciiEscape::with_preferred_quote(s, flags.quote_style());
if let Some(len) = escape.layout().len {
self.buffer.reserve(len);
}
@@ -192,9 +176,7 @@ impl<'a> Generator<'a> {
return;
}
self.p(flags.prefix().as_str());
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
let escape = UnicodeEscape::with_preferred_quote(s, quote_style);
let escape = UnicodeEscape::with_preferred_quote(s, flags.quote_style());
if let Some(len) = escape.layout().len {
self.buffer.reserve(len);
}
@@ -1524,9 +1506,7 @@ impl<'a> Generator<'a> {
self.buffer += &s;
return;
}
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
let escape = UnicodeEscape::with_preferred_quote(&s, quote_style);
let escape = UnicodeEscape::with_preferred_quote(&s, flags.quote_style());
if let Some(len) = escape.layout().len {
self.buffer.reserve(len);
}
@@ -1551,9 +1531,6 @@ impl<'a> Generator<'a> {
flags: AnyStringFlags,
) {
self.p(flags.prefix().as_str());
let flags =
flags.with_quote_style(self.preferred_quote.unwrap_or_else(|| flags.quote_style()));
self.p(flags.quote_str());
self.unparse_interpolated_string_body(values, flags);
self.p(flags.quote_str());
@@ -1586,7 +1563,6 @@ impl<'a> Generator<'a> {
#[cfg(test)]
mod tests {
use ruff_python_ast::str::Quote;
use ruff_python_ast::{Mod, ModModule};
use ruff_python_parser::{self, Mode, ParseOptions, parse_module};
use ruff_source_file::LineEnding;
@@ -1604,17 +1580,15 @@ mod tests {
generator.generate()
}
/// Like [`round_trip`] but configure the [`Generator`] with the requested
/// `indentation`, `line_ending` and `preferred_quote` settings.
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and
/// `line_ending` settings.
fn round_trip_with(
indentation: &Indentation,
line_ending: LineEnding,
preferred_quote: Option<Quote>,
contents: &str,
) -> String {
let module = parse_module(contents).unwrap();
let mut generator =
Generator::new(indentation, line_ending).with_preferred_quote(preferred_quote);
let mut generator = Generator::new(indentation, line_ending);
generator.unparse_suite(module.suite());
generator.generate()
}
@@ -2000,7 +1974,6 @@ if True:
round_trip_with(
&Indentation::new(" ".to_string()),
LineEnding::default(),
None,
r"
if True:
pass
@@ -2018,7 +1991,6 @@ if True:
round_trip_with(
&Indentation::new(" ".to_string()),
LineEnding::default(),
None,
r"
if True:
pass
@@ -2036,7 +2008,6 @@ if True:
round_trip_with(
&Indentation::new("\t".to_string()),
LineEnding::default(),
None,
r"
if True:
pass
@@ -2058,7 +2029,6 @@ if True:
round_trip_with(
&Indentation::default(),
LineEnding::Lf,
None,
"if True:\n print(42)",
),
"if True:\n print(42)",
@@ -2068,7 +2038,6 @@ if True:
round_trip_with(
&Indentation::default(),
LineEnding::CrLf,
None,
"if True:\n print(42)",
),
"if True:\r\n print(42)",
@@ -2078,32 +2047,9 @@ if True:
round_trip_with(
&Indentation::default(),
LineEnding::Cr,
None,
"if True:\n print(42)",
),
"if True:\r print(42)",
);
}
#[test_case::test_case(r#""'hello'""#, r#""'hello'""#, Quote::Single ; "basic str ignored")]
#[test_case::test_case(r#"b"'hello'""#, r#"b"'hello'""#, Quote::Single ; "basic bytes ignored")]
#[test_case::test_case("'hello'", r#""hello""#, Quote::Double ; "basic str double")]
#[test_case::test_case(r#""hello""#, "'hello'", Quote::Single ; "basic str single")]
#[test_case::test_case("b'hello'", r#"b"hello""#, Quote::Double ; "basic bytes double")]
#[test_case::test_case(r#"b"hello""#, "b'hello'", Quote::Single ; "basic bytes single")]
#[test_case::test_case(r#""hello""#, r#""hello""#, Quote::Double ; "remain str double")]
#[test_case::test_case("'hello'", "'hello'", Quote::Single ; "remain str single")]
#[test_case::test_case("x: list['str']", r#"x: list["str"]"#, Quote::Double ; "type ann double")]
#[test_case::test_case(r#"x: list["str"]"#, "x: list['str']", Quote::Single ; "type ann single")]
#[test_case::test_case("f'hello'", r#"f"hello""#, Quote::Double ; "basic fstring double")]
#[test_case::test_case(r#"f"hello""#, "f'hello'", Quote::Single ; "basic fstring single")]
fn preferred_quote(inp: &str, out: &str, quote: Quote) {
let got = round_trip_with(
&Indentation::default(),
LineEnding::default(),
Some(quote),
inp,
);
assert_eq!(got, out);
}
}

View File

@@ -1613,7 +1613,7 @@ pub(super) fn needs_chaperone_space(
if is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled(context) {
if flags.is_triple_quoted() {
if let Some(before_quote) = trim_end.strip_suffix(flags.quote_style().as_char()) {
if count_consecutive_chars_from_end(before_quote, '\\').is_multiple_of(2) {
if count_consecutive_chars_from_end(before_quote, '\\') % 2 == 0 {
// Even backslash count preceding quote;
// ```py
// """a " """

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.13.1"
version = "0.13.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -46,7 +46,6 @@ class MDTestRunner:
CRATE_NAME,
"--no-run",
"--color=always",
"--test=mdtest",
"--message-format",
message_format,
],

View File

@@ -752,36 +752,6 @@ b_container = ClassContainer[B](B)
a_instance: A = use_a_class_container(b_container) # This should work
```
## TypeIs
```toml
[environment]
python-version = "3.13"
```
`TypeIs[T]` is invariant in `T`. See the [typing spec][typeis-spec] for a justification.
```py
from typing import TypeIs
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
class A:
pass
class B(A):
pass
class C[T]:
def check(x: object) -> TypeIs[T]:
# this is a bad check, but we only care about it type-checking
return False
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B]))
```
## Inheriting from generic classes with inferred variance
When inheriting from a generic class with our type variable substituted in, we count its occurrences
@@ -867,4 +837,3 @@ static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B]))
[linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
[typeis-spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis

View File

@@ -74,25 +74,52 @@ from typing import Any as Any, Literal as Literal
Here, none of the symbols are being re-exported in the stub file.
In this case the symbols shouldn't be available as imports or attributes.
```py
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
from a import b
# error: [unresolved-attribute] "no attribute `Any`"
reveal_type(b.Any) # revealed: Unknown
# error: [unresolved-attribute] "no attribute `Literal`"
reveal_type(b.Literal) # revealed: Unknown
# error: [unresolved-attribute] "no attribute `foo`"
reveal_type(b.foo) # revealed: Unknown
# error: [unresolved-attribute] "no attribute `bar`"
reveal_type(b.bar) # revealed: Unknown
# error: [unresolved-import] "Module `a.b` has no member `foo`"
# error: [unresolved-import] "Module `a.b` has no member `bar`"
# error: [unresolved-import] "Module `a.b` has no member `Any`"
# error: [unresolved-import] "Module `a.b` has no member `Literal`"
from a.b import foo, bar, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
reveal_type(bar) # revealed: Unknown
```
`b.pyi`:
`a/__init__.pyi`:
```pyi
import foo
```
`a/b.pyi`:
```pyi
import a.foo
from . import bar
from typing import Any, Literal
```
`foo.pyi`:
`a/foo.pyi`:
```pyi
```
`a/bar.pyi`:
```pyi
@@ -261,39 +288,93 @@ reveal_type(Foo) # revealed: Unknown
## Re-exports in `__init__.pyi`
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
but the inference would be `Unknown`.
Within `__init__.pyi` relative imports (`from . import xyz` or `from .pub import xyz`) are also
treated as a re-exports.
We check the both the members of the module and the imports of the module as you _should_ be able to
do `from a import priv` but the attribute `a.priv` _should not_ exist.
The most subtle detail here is whether `from .semipriv import Pub` should make the `a.semipriv`
attribute exist or not. We do not currently do this, although perhaps we should.
```py
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
import a
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
reveal_type(a.Pub) # revealed: <class 'Pub'>
# error: [unresolved-attribute]
reveal_type(a.Priv) # revealed: Unknown
reveal_type(a.pub) # revealed: <module 'a.pub'>
# error: [unresolved-attribute]
reveal_type(a.priv) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(a.semipriv) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(a.sub) # revealed: Unknown
reveal_type(a.subpub) # revealed: <module 'a.sub.subpub'>
# error: [unresolved-attribute]
reveal_type(a.subpriv) # revealed: Unknown
# error: [unresolved-import] "Priv"
from a import Pub, Priv
# error: [unresolved-import] "subpriv"
from a import pub, priv, semipriv, sub, subpub, subpriv
reveal_type(Pub) # revealed: <class 'Pub'>
reveal_type(Priv) # revealed: Unknown
reveal_type(pub) # revealed: <module 'a.pub'>
reveal_type(priv) # revealed: <module 'a.priv'>
reveal_type(semipriv) # revealed: <module 'a.semipriv'>
reveal_type(sub) # revealed: <module 'a.sub'>
reveal_type(subpub) # revealed: <module 'a.sub.subpub'>
reveal_type(subpriv) # revealed: Unknown
```
`a/__init__.pyi`:
```pyi
from .b import c
from .foo import Foo
# re-exported because they're relative
from .sub import subpub
from .semipriv import Pub
from . import pub
# not re-exported because they're absolute
from a.sub import subpriv
from a.semipriv import Priv
from a import priv
```
`a/foo.pyi`:
`a/pub.pyi`:
```pyi
class Foo: ...
```
`a/b/__init__.pyi`:
`a/priv.pyi`:
```pyi
```
`a/semipriv.pyi`:
```pyi
class Pub: ...
class Priv: ...
```
`a/sub/__init__.pyi`:
```pyi
```
`a/b/c.pyi`:
`a/sub/subpub.pyi`:
```pyi
```
`a/sub/subpriv.pyi`:
```pyi

View File

@@ -331,7 +331,11 @@ pub(crate) fn imported_symbol<'db>(
) -> PlaceAndQualifiers<'db> {
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
if file.is_stub(db) {
RequiresExplicitReExport::Yes
if file.is_init(db) {
RequiresExplicitReExport::YesButInitIdiomAllowed
} else {
RequiresExplicitReExport::Yes
}
} else {
RequiresExplicitReExport::No
}
@@ -932,7 +936,8 @@ fn place_from_bindings_impl<'db>(
let mut bindings_with_constraints = bindings_with_constraints.peekable();
let is_non_exported = |binding: Definition<'db>| {
requires_explicit_reexport.is_yes() && !is_reexported(db, binding)
requires_explicit_reexport.is_yes()
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, binding))
};
let unbound_reachability_constraint = match bindings_with_constraints.peek() {
@@ -1209,7 +1214,8 @@ fn place_from_declarations_impl<'db>(
let mut exactly_one_declaration = false;
let is_non_exported = |declaration: Definition<'db>| {
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
requires_explicit_reexport.is_yes()
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, declaration))
};
let undeclared_reachability = match declarations.peek() {
@@ -1320,21 +1326,26 @@ fn place_from_declarations_impl<'db>(
// This will first check if the definition is using the "redundant alias" pattern like `import foo
// as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being
// exported via `__all__`.
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> ReExportKind {
// This information is computed by the semantic index builder.
if definition.is_reexported(db) {
return true;
let reexported = definition.is_reexported(db);
if reexported != ReExportKind::No {
return reexported;
}
// At this point, the definition should either be an `import` or `from ... import` statement.
// This is because the default value of `is_reexported` is `true` for any other kind of
// definition.
let Some(all_names) = dunder_all_names(db, definition.file(db)) else {
return false;
return ReExportKind::No;
};
let table = place_table(db, definition.scope(db));
let symbol_id = definition.place(db).expect_symbol();
let symbol_name = table.symbol(symbol_id).name();
all_names.contains(symbol_name)
if all_names.contains(symbol_name) {
ReExportKind::Yes
} else {
ReExportKind::No
}
}
mod implicit_globals {
@@ -1500,13 +1511,35 @@ mod implicit_globals {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum RequiresExplicitReExport {
Yes,
No,
/// This is an `__init__.pyi` and `from . import b` is considered a re-export
YesButInitIdiomAllowed,
Yes,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ReExportKind {
No,
/// `b` in `from . import b`
InitIdiom,
Yes,
}
impl get_size2::GetSize for ReExportKind {}
impl RequiresExplicitReExport {
/// Whether re-exports are necessary at all (this is really "is not No")
const fn is_yes(self) -> bool {
matches!(self, RequiresExplicitReExport::Yes)
!matches!(self, RequiresExplicitReExport::No)
}
/// Whether the style of re-export is sufficient for the context
fn is_satisfied(self, reexport: ReExportKind) -> bool {
match self {
RequiresExplicitReExport::No => true,
RequiresExplicitReExport::YesButInitIdiomAllowed => reexport != ReExportKind::No,
RequiresExplicitReExport::Yes => reexport == ReExportKind::Yes,
}
}
}

View File

@@ -20,6 +20,7 @@ use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
use crate::module_resolver::resolve_module;
use crate::node_key::NodeKey;
use crate::place::ReExportKind;
use crate::semantic_index::ast_ids::AstIdsBuilder;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::definition::{
@@ -1436,6 +1437,12 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
(Name::new(alias.name.id.split('.').next().unwrap()), false)
};
let is_reexported = if is_reexported {
ReExportKind::Yes
} else {
ReExportKind::No
};
let symbol = self.add_symbol(symbol_name);
self.add_definition(
symbol.into(),
@@ -1562,6 +1569,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
(&alias.name.id, false)
};
let is_reexported = if is_reexported {
ReExportKind::Yes
} else if node.level == 1 {
// `from . import a`
ReExportKind::InitIdiom
} else {
ReExportKind::No
};
// Look for imports `from __future__ import annotations`, ignore `as ...`
// We intentionally don't enforce the rules about location of `__future__`
// imports here, we assume the user's intent was to apply the `__future__`

View File

@@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Db;
use crate::ast_node_ref::AstNodeRef;
use crate::node_key::NodeKey;
use crate::place::ReExportKind;
use crate::semantic_index::place::ScopedPlaceId;
use crate::semantic_index::scope::{FileScopeId, ScopeId};
use crate::semantic_index::symbol::ScopedSymbolId;
@@ -41,7 +42,7 @@ pub struct Definition<'db> {
pub kind: DefinitionKind<'db>,
/// This is a dedicated field to avoid accessing `kind` to compute this value.
pub(crate) is_reexported: bool,
pub(crate) is_reexported: ReExportKind,
}
// The Salsa heap is tracked separately.
@@ -337,7 +338,7 @@ impl<'ast> From<StarImportDefinitionNodeRef<'ast>> for DefinitionNodeRef<'ast, '
pub(crate) struct ImportDefinitionNodeRef<'ast> {
pub(crate) node: &'ast ast::StmtImport,
pub(crate) alias_index: usize,
pub(crate) is_reexported: bool,
pub(crate) is_reexported: ReExportKind,
}
#[derive(Copy, Clone, Debug)]
@@ -350,7 +351,7 @@ pub(crate) struct StarImportDefinitionNodeRef<'ast> {
pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
pub(crate) node: &'ast ast::StmtImportFrom,
pub(crate) alias_index: usize,
pub(crate) is_reexported: bool,
pub(crate) is_reexported: ReExportKind,
}
#[derive(Copy, Clone, Debug)]
@@ -678,11 +679,11 @@ pub enum DefinitionKind<'db> {
}
impl DefinitionKind<'_> {
pub(crate) fn is_reexported(&self) -> bool {
pub(crate) fn is_reexported(&self) -> ReExportKind {
match self {
DefinitionKind::Import(import) => import.is_reexported(),
DefinitionKind::ImportFrom(import) => import.is_reexported(),
_ => true,
_ => ReExportKind::Yes,
}
}
@@ -956,7 +957,7 @@ impl<'db> ComprehensionDefinitionKind<'db> {
pub struct ImportDefinitionKind {
node: AstNodeRef<ast::StmtImport>,
alias_index: usize,
is_reexported: bool,
is_reexported: ReExportKind,
}
impl ImportDefinitionKind {
@@ -968,7 +969,7 @@ impl ImportDefinitionKind {
&self.node.node(module).names[self.alias_index]
}
pub(crate) fn is_reexported(&self) -> bool {
pub(crate) fn is_reexported(&self) -> ReExportKind {
self.is_reexported
}
}
@@ -977,7 +978,7 @@ impl ImportDefinitionKind {
pub struct ImportFromDefinitionKind {
node: AstNodeRef<ast::StmtImportFrom>,
alias_index: usize,
is_reexported: bool,
is_reexported: ReExportKind,
}
impl ImportFromDefinitionKind {
@@ -989,7 +990,7 @@ impl ImportFromDefinitionKind {
&self.node.node(module).names[self.alias_index]
}
pub(crate) fn is_reexported(&self) -> bool {
pub(crate) fn is_reexported(&self) -> ReExportKind {
self.is_reexported
}
}

View File

@@ -6620,7 +6620,6 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
.map(|ty| ty.variance_of(db, typevar))
.collect(),
Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, typevar),
Type::TypeIs(type_is_type) => type_is_type.variance_of(db, typevar),
Type::Dynamic(_)
| Type::Never
| Type::WrapperDescriptor(_)
@@ -6641,6 +6640,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
| Type::BoundSuper(_)
| Type::TypeVar(_)
| Type::NonInferableTypeVar(_)
| Type::TypeIs(_)
| Type::TypedDict(_)
| Type::TypeAlias(_) => TypeVarVariance::Bivariant,
};
@@ -10956,16 +10956,6 @@ impl<'db> TypeIsType<'db> {
}
}
impl<'db> VarianceInferable<'db> for TypeIsType<'db> {
// See the [typing spec] on why `TypeIs` is invariant in its type.
// [typing spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
self.return_type(db)
.with_polarity(TypeVarVariance::Invariant)
.variance_of(db, typevar)
}
}
/// Walk the MRO of this class and return the last class just before the specified known base.
/// This can be used to determine upper bounds for `Self` type variables on methods that are
/// being added to the given class.

View File

@@ -87,8 +87,8 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
sort_text: Some(format!("{i:-max_index_len$}")),
detail: type_display.clone(),
label_details: Some(CompletionItemLabelDetails {
detail: comp.module_name.map(|name| format!(" (import {name})")),
description: type_display,
detail: type_display,
description: comp.module_name.map(ToString::to_string),
}),
insert_text: comp.insert.map(String::from),
additional_text_edits: import_edit.map(|edit| vec![edit]),

View File

@@ -250,7 +250,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> {
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
let checked = self.checked_files.fetch_add(1, Ordering::Relaxed) + 1;
if checked.is_multiple_of(100) || checked == self.total_files {
if checked % 100 == 0 || checked == self.total_files {
// Report progress every 100 files or when all files are checked
self.report_progress();
}

View File

@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
stage: build
interruptible: true
image:
name: ghcr.io/astral-sh/ruff:0.13.1-alpine
name: ghcr.io/astral-sh/ruff:0.13.0-alpine
before_script:
- cd $CI_PROJECT_DIR
- ruff --version
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff-check
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff-check
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.13.1"
version = "0.13.0"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"
@@ -111,11 +111,30 @@ force-exclude = '''
major_labels = [] # Ruff never uses the major version number
minor_labels = ["breaking"] # Bump the minor version on breaking changes
ignore_labels = ["internal", "ci", "testing", "ty"]
changelog_ignore_labels = ["internal", "ci", "testing", "ty"]
changelog_sections.breaking = "Breaking changes"
changelog_sections.preview = "Preview features"
changelog_sections.bug = "Bug fixes"
changelog_sections.rule = "Rule changes"
changelog_sections.diagnostics = "Rule changes"
changelog_sections.docstring = "Rule changes"
changelog_sections.fixes = "Rule changes"
changelog_sections.isort = "Rule changes"
changelog_sections.performance = "Performance"
changelog_sections.formatter = "Formatter"
changelog_sections.server = "Server"
changelog_sections.cli = "CLI"
changelog_sections.configuration = "Configuration"
changelog_sections.documentation = "Documentation"
changelog_sections.__unknown__ = "Other changes"
# We exclude contributors from the CHANGELOG file
# Generate separately with `rooster contributors` for the GitHub release page
changelog_contributors = false
version_files = [
"README.md",
"pyproject.toml",
"docs/integrations.md",
"docs/tutorial.md",
"crates/ruff/Cargo.toml",
@@ -123,22 +142,3 @@ version_files = [
"crates/ruff_wasm/Cargo.toml",
"scripts/benchmarks/pyproject.toml",
]
[tool.rooster.section-labels]
"Breaking changes" = ["breaking"]
"Preview features" = ["preview"]
"Bug fixes" = ["bug"]
"Rule changes" = [
"diagnostics",
"docstrings",
"rule",
"fixes",
"isort",
]
"Performance" = ["performance"]
"Formatter" = ["formatter"]
"Server" = ["server"]
"CLI" = ["cli"]
"Configuration" = ["configuration"]
"Documentation" = ["documentation"]
"Other changes" = ["__unknown__"]

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "1.90"
channel = "1.89"

View File

@@ -1,6 +1,6 @@
[project]
name = "scripts"
version = "0.13.1"
version = "0.13.0"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]

View File

@@ -11,8 +11,14 @@ project_root="$(dirname "$script_root")"
echo "Updating metadata with rooster..."
cd "$project_root"
uvx --python 3.12 --isolated -- \
rooster@0.0.10a1 release "$@"
uv tool run --from 'rooster-blue>=0.0.7' --python 3.12 --isolated -- \
rooster release "$@"
echo "Updating lockfile..."
cargo update -p ruff
echo "Generating contributors list..."
echo ""
echo ""
uv tool run --from 'rooster-blue>=0.0.7' --python 3.12 --isolated -- \
rooster contributors --quiet