Skip to content

Commit

Permalink
Accept a PEP 440 version specifier for required-version
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 3, 2024
1 parent db25a56 commit 1b95d07
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 40 deletions.
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
156 changes: 155 additions & 1 deletion crates/ruff/tests/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ fn deprecated_config_option_overridden_via_cli() {
success: false
exit_code: 1
----- stdout -----
-:1:7: N801 Class name `lowercase` should use CapWords convention
-:1:7: N801 Class name `lowercase` should use CapWords convention
Found 1 error.
----- stderr -----
Expand Down Expand Up @@ -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(())
}
1 change: 0 additions & 1 deletion crates/ruff_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
42 changes: 32 additions & 10 deletions crates/ruff_linter/src/settings/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}
}

Expand Down
22 changes: 14 additions & 8 deletions crates/ruff_workspace/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::{
Expand Down Expand Up @@ -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>,

Expand Down Expand Up @@ -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
));
}
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 12 additions & 7 deletions crates/ruff_workspace/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 1b95d07

Please sign in to comment.