mirror of https://github.com/astral-sh/ruff
Accept a PEP 440 version specifier for required-version (#10216)
## Summary Allows `required-version` to be set with a version specifier, like `>=0.3.1`. If a single version is provided, falls back to assuming `==0.3.1`, for backwards compatibility. Closes https://github.com/astral-sh/ruff/issues/10192.
This commit is contained in:
parent
db25a563f7
commit
84bf333031
|
|
@ -2158,7 +2158,6 @@ dependencies = [
|
|||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
|
|
@ -2581,12 +2580,6 @@ version = "4.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ result-like = { version = "0.5.0" }
|
|||
rustc-hash = { version = "1.1.0" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
semver = { version = "1.0.22" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde-wasm-bindgen = { version = "0.6.4" }
|
||||
serde_json = { version = "1.0.113" }
|
||||
|
|
|
|||
|
|
@ -972,3 +972,157 @@ import os
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_exact_mismatch() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
required-version = "0.1.0"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_exact_match() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
format!(
|
||||
r#"
|
||||
required-version = "{version}"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:2:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_bound_mismatch() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
format!(
|
||||
r#"
|
||||
required-version = ">{version}"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_bound_match() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
required-version = ">=0.1.0"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:2:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ regex = { workspace = true }
|
|||
result-like = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::string::ToString;
|
|||
|
||||
use anyhow::{bail, Result};
|
||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use pep440_rs::{Version as Pep440Version, VersionSpecifiers};
|
||||
use pep440_rs::{Version as Pep440Version, VersionSpecifier, VersionSpecifiers};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use strum::IntoEnumIterator;
|
||||
|
|
@ -536,22 +536,44 @@ impl SerializationFormat {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
#[serde(try_from = "String")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Version(String);
|
||||
pub struct RequiredVersion(VersionSpecifiers);
|
||||
|
||||
impl TryFrom<String> for Version {
|
||||
type Error = semver::Error;
|
||||
impl TryFrom<String> for RequiredVersion {
|
||||
type Error = pep440_rs::Pep440Error;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
semver::Version::parse(&value).map(|_| Self(value))
|
||||
// Treat `0.3.1` as `==0.3.1`, for backwards compatibility.
|
||||
if let Ok(version) = pep440_rs::Version::from_str(&value) {
|
||||
Ok(Self(VersionSpecifiers::from(
|
||||
VersionSpecifier::equals_version(version),
|
||||
)))
|
||||
} else {
|
||||
Ok(Self(VersionSpecifiers::from_str(&value)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Version {
|
||||
type Target = str;
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for RequiredVersion {
|
||||
fn schema_name() -> String {
|
||||
"RequiredVersion".to_string()
|
||||
}
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
gen.subschema_for::<String>()
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion {
|
||||
/// Return `true` if the given version is required.
|
||||
pub fn contains(&self, version: &pep440_rs::Version) -> bool {
|
||||
self.0.contains(version)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RequiredVersion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ use std::borrow::Cow;
|
|||
use std::env::VarError;
|
||||
use std::num::{NonZeroU16, NonZeroU8};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use glob::{glob, GlobError, Paths, PatternError};
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use ruff_linter::settings::fix_safety_table::FixSafetyTable;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use shellexpand;
|
||||
use shellexpand::LookupError;
|
||||
|
|
@ -24,10 +24,11 @@ use ruff_linter::registry::RuleNamespace;
|
|||
use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES};
|
||||
use ruff_linter::rule_selector::{PreviewOptions, Specificity};
|
||||
use ruff_linter::rules::pycodestyle;
|
||||
use ruff_linter::settings::fix_safety_table::FixSafetyTable;
|
||||
use ruff_linter::settings::rule_table::RuleTable;
|
||||
use ruff_linter::settings::types::{
|
||||
ExtensionMapping, FilePattern, FilePatternSet, PerFileIgnore, PerFileIgnores, PreviewMode,
|
||||
PythonVersion, SerializationFormat, UnsafeFixes, Version,
|
||||
PythonVersion, RequiredVersion, SerializationFormat, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
|
||||
use ruff_linter::{
|
||||
|
|
@ -116,7 +117,7 @@ pub struct Configuration {
|
|||
pub unsafe_fixes: Option<UnsafeFixes>,
|
||||
pub output_format: Option<SerializationFormat>,
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub required_version: Option<Version>,
|
||||
pub required_version: Option<RequiredVersion>,
|
||||
pub extension: Option<ExtensionMapping>,
|
||||
pub show_fixes: Option<bool>,
|
||||
|
||||
|
|
@ -145,10 +146,12 @@ pub struct Configuration {
|
|||
impl Configuration {
|
||||
pub fn into_settings(self, project_root: &Path) -> Result<Settings> {
|
||||
if let Some(required_version) = &self.required_version {
|
||||
if &**required_version != RUFF_PKG_VERSION {
|
||||
let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
|
||||
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
|
||||
if !required_version.contains(&ruff_pkg_version) {
|
||||
return Err(anyhow!(
|
||||
"Required version `{}` does not match the running version `{}`",
|
||||
&**required_version,
|
||||
required_version,
|
||||
RUFF_PKG_VERSION
|
||||
));
|
||||
}
|
||||
|
|
@ -1467,15 +1470,18 @@ fn warn_about_deprecated_top_level_lint_options(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::configuration::{LintConfiguration, RuleSelection};
|
||||
use crate::options::PydocstyleOptions;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_linter::codes::{Flake8Copyright, Pycodestyle, Refurb};
|
||||
use ruff_linter::registry::{Linter, Rule, RuleSet};
|
||||
use ruff_linter::rule_selector::PreviewOptions;
|
||||
use ruff_linter::settings::types::PreviewMode;
|
||||
use ruff_linter::RuleSelector;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::configuration::{LintConfiguration, RuleSelection};
|
||||
use crate::options::PydocstyleOptions;
|
||||
|
||||
const PREVIEW_RULES: &[Rule] = &[
|
||||
Rule::IsinstanceTypeNone,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::options_base::{OptionsMetadata, Visit};
|
||||
use ruff_formatter::IndentStyle;
|
||||
use ruff_linter::line_width::{IndentWidth, LineLength};
|
||||
use ruff_linter::rules::flake8_pytest_style::settings::SettingsError;
|
||||
|
|
@ -25,12 +24,13 @@ use ruff_linter::rules::{
|
|||
pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade,
|
||||
};
|
||||
use ruff_linter::settings::types::{
|
||||
IdentifierPattern, PythonVersion, SerializationFormat, Version,
|
||||
IdentifierPattern, PythonVersion, RequiredVersion, SerializationFormat,
|
||||
};
|
||||
use ruff_linter::{warn_user_once, RuleSelector};
|
||||
use ruff_macros::{CombineOptions, OptionsMetadata};
|
||||
use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle};
|
||||
|
||||
use crate::options_base::{OptionsMetadata, Visit};
|
||||
use crate::settings::LineEnding;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, Serialize, Deserialize)]
|
||||
|
|
@ -135,17 +135,22 @@ pub struct Options {
|
|||
)]
|
||||
pub show_fixes: Option<bool>,
|
||||
|
||||
/// Require a specific version of Ruff to be running (useful for unifying
|
||||
/// results across many environments, e.g., with a `pyproject.toml`
|
||||
/// file).
|
||||
/// Enforce a requirement on the version of Ruff, to enforce at runtime.
|
||||
/// If the version of Ruff does not meet the requirement, Ruff will exit
|
||||
/// with an error.
|
||||
///
|
||||
/// Useful for unifying results across many environments, e.g., with a
|
||||
/// `pyproject.toml` file.
|
||||
///
|
||||
/// Accepts a PEP 440 specifier, like `==0.3.1` or `>=0.3.1`.
|
||||
#[option(
|
||||
default = "null",
|
||||
value_type = "str",
|
||||
example = r#"
|
||||
required-version = "0.0.193"
|
||||
required-version = ">=0.0.193"
|
||||
"#
|
||||
)]
|
||||
pub required_version: Option<Version>,
|
||||
pub required_version: Option<RequiredVersion>,
|
||||
|
||||
/// Whether to enable preview mode. When preview mode is enabled, Ruff will
|
||||
/// use unstable rules, fixes, and formatting.
|
||||
|
|
|
|||
|
|
@ -623,10 +623,10 @@
|
|||
]
|
||||
},
|
||||
"required-version": {
|
||||
"description": "Require a specific version of Ruff to be running (useful for unifying results across many environments, e.g., with a `pyproject.toml` file).",
|
||||
"description": "Enforce a requirement on the version of Ruff, to enforce at runtime. If the version of Ruff does not meet the requirement, Ruff will exit with an error.\n\nUseful for unifying results across many environments, e.g., with a `pyproject.toml` file.\n\nAccepts a PEP 440 specifier, like `==0.3.1` or `>=0.3.1`.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Version"
|
||||
"$ref": "#/definitions/RequiredVersion"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
|
@ -2590,6 +2590,9 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"RequiredVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"RuleSelector": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
@ -3870,9 +3873,6 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue