From 8bcfdb0ed0a6345afdcd5b7ff421f92be29fac9a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 22 Dec 2022 14:45:45 -0500 Subject: [PATCH] Add --required-version --- Cargo.lock | 5 +++-- Cargo.toml | 1 + README.md | 18 ++++++++++++++++++ flake8_to_ruff/src/converter.rs | 7 +++++++ pyproject.toml | 4 ++++ ruff.schema.json | 7 +++++++ src/commands.rs | 33 ++++++++++++++++++++++++--------- src/linter.rs | 17 ++++++++++++++++- src/main.rs | 6 ++++++ src/resolver.rs | 20 ++++++++++++++++++++ src/settings/configuration.rs | 9 +++++++-- src/settings/mod.rs | 25 +++++++++++++++++++++++-- src/settings/options.rs | 13 ++++++++++++- src/settings/pyproject.rs | 6 ++++++ src/settings/types.rs | 21 +++++++++++++++++++++ 15 files changed, 175 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7822bd527a4845..b4283a6ddc02b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1902,6 +1902,7 @@ dependencies = [ "rustpython-common", "rustpython-parser", "schemars", + "semver", "serde", "serde_json", "shellexpand", @@ -2122,9 +2123,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" diff --git a/Cargo.toml b/Cargo.toml index a8f3fc15e12b32..4d6ac1016bc200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/ rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "1b6cb170e925a43d605b3fed9f6b878e63e47744" } rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "1b6cb170e925a43d605b3fed9f6b878e63e47744" } schemars = { version = "0.8.11" } +semver = { version = "1.0.16" } serde = { version = "1.0.147", features = ["derive"] } serde_json = { version = "1.0.87" } shellexpand = { version = "3.0.0" } diff --git a/README.md b/README.md index b5b69de58949d9..421f5cdefcc415 100644 --- a/README.md +++ b/README.md @@ -1948,6 +1948,24 @@ when considering any matching files. --- +#### [`required-version`](#required-version) + +Require a specific version of Ruff to be running (useful for unifying results across +many environments, e.g., with a `pyproject.toml` file). + +**Default value**: `None` + +**Type**: `String` + +**Example usage**: + +```toml +[tool.ruff] +required-version = "0.0.193" +``` + +--- + #### [`respect-gitignore`](#respect-gitignore) Whether to automatically exclude files that are ignored by `.ignore`, diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index b03409f3072a47..4c46b78782839e 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -302,6 +302,7 @@ mod tests { ignore_init_module_imports: None, line_length: None, per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::E, @@ -357,6 +358,7 @@ mod tests { ignore_init_module_imports: None, line_length: Some(100), per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::E, @@ -412,6 +414,7 @@ mod tests { ignore_init_module_imports: None, line_length: Some(100), per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::E, @@ -467,6 +470,7 @@ mod tests { ignore_init_module_imports: None, line_length: None, per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::E, @@ -522,6 +526,7 @@ mod tests { ignore_init_module_imports: None, line_length: None, per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::E, @@ -585,6 +590,7 @@ mod tests { ignore_init_module_imports: None, line_length: None, per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::D100, @@ -676,6 +682,7 @@ mod tests { ignore_init_module_imports: None, line_length: None, per_file_ignores: None, + required_version: None, respect_gitignore: None, select: Some(vec![ CheckCodePrefix::E, diff --git a/pyproject.toml b/pyproject.toml index cd69b1abe89734..32941757d3428b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,11 @@ bindings = "bin" strip = true [tool.ruff] +#required-version = "0.0.192" [tool.ruff.isort] force-wrap-aliases = true combine-as-imports = true + +[tool.black] +required-version = "22.12.1" diff --git a/ruff.schema.json b/ruff.schema.json index 3c43892cf1b328..2336918d194516 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -281,6 +281,13 @@ } ] }, + "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).", + "type": [ + "string", + "null" + ] + }, "respect-gitignore": { "description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.", "type": [ diff --git a/src/commands.rs b/src/commands.rs index 1205a86133e80e..fcaceefe05971f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -38,14 +38,8 @@ pub fn run( let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); - // Discover the package root for each Python file. - let package_roots = packages::detect_package_roots( - &paths - .iter() - .flatten() - .map(ignore::DirEntry::path) - .collect::>(), - ); + // Validate the `Settings` and return any errors. + resolver.validate(pyproject_strategy)?; // Initialize the cache. if matches!(cache, flags::Cache::Enabled) { @@ -71,6 +65,15 @@ pub fn run( } }; + // Discover the package root for each Python file. + let package_roots = packages::detect_package_roots( + &paths + .iter() + .flatten() + .map(ignore::DirEntry::path) + .collect::>(), + ); + let start = Instant::now(); let mut diagnostics: Diagnostics = par_iter(&paths) .map(|entry| { @@ -176,6 +179,9 @@ pub fn add_noqa( let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); + // Validate the `Settings` and return any errors. + resolver.validate(pyproject_strategy)?; + let start = Instant::now(); let modifications: usize = par_iter(&paths) .flatten() @@ -212,6 +218,9 @@ pub fn autoformat( let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); + // Validate the `Settings` and return any errors. + resolver.validate(pyproject_strategy)?; + let start = Instant::now(); let modifications = par_iter(&paths) .flatten() @@ -245,6 +254,9 @@ pub fn show_settings( let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?; + // Validate the `Settings` and return any errors. + resolver.validate(pyproject_strategy)?; + // Print the list of files. let Some(entry) = paths .iter() @@ -268,9 +280,12 @@ pub fn show_files( overrides: &Overrides, ) -> Result<()> { // Collect all files in the hierarchy. - let (paths, _resolver) = + let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?; + // Validate the `Settings` and return any errors. + resolver.validate(pyproject_strategy)?; + // Print the list of files. for entry in paths .iter() diff --git a/src/linter.rs b/src/linter.rs index 973a33fe091578..0c142f7e631228 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -59,6 +59,9 @@ pub(crate) fn check_path( autofix: flags::Autofix, noqa: flags::Noqa, ) -> Result> { + // Validate the `Settings` and return any errors. + settings.validate()?; + // Aggregate all checks. let mut checks: Vec = vec![]; @@ -175,6 +178,9 @@ pub fn lint_path( cache: flags::Cache, autofix: fixer::Mode, ) -> Result { + // Validate the `Settings` and return any errors. + settings.validate()?; + let metadata = path.metadata()?; // Check the cache. @@ -202,6 +208,9 @@ pub fn lint_path( /// Add any missing `#noqa` pragmas to the source code at the given `Path`. pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result { + // Validate the `Settings` and return any errors. + settings.validate()?; + // Read the file from disk. let contents = fs::read_file(path)?; @@ -241,7 +250,10 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result { } /// Apply autoformatting to the source code at the given `Path`. -pub fn autoformat_path(path: &Path, _settings: &Settings) -> Result<()> { +pub fn autoformat_path(path: &Path, settings: &Settings) -> Result<()> { + // Validate the `Settings` and return any errors. + settings.validate()?; + // Read the file from disk. let contents = fs::read_file(path)?; @@ -266,6 +278,9 @@ pub fn lint_stdin( settings: &Settings, autofix: fixer::Mode, ) -> Result { + // Validate the `Settings` and return any errors. + settings.validate()?; + // Read the file from disk. let contents = stdin.to_string(); diff --git a/src/main.rs b/src/main.rs index 4c790757164e01..c7127bee3fc14f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,6 +100,12 @@ fn inner_main() -> Result { cli.stdin_filename.as_deref(), )?; + // Validate the `Settings` and return any errors. + match &pyproject_strategy { + PyprojectDiscovery::Fixed(settings) => settings.validate()?, + PyprojectDiscovery::Hierarchical(settings) => settings.validate()?, + }; + // Extract options that are included in `Settings`, but only apply at the top // level. let file_strategy = FileDiscovery { diff --git a/src/resolver.rs b/src/resolver.rs index c3651d082e4f85..d3da930e59b7f4 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -91,6 +91,26 @@ impl Resolver { pub fn iter(&self) -> impl Iterator { self.settings.values() } + + /// Validate all resolved `Settings` in this `Resolver`. + pub fn validate(&self, strategy: &PyprojectDiscovery) -> Result<()> { + // TODO(charlie): This risks false positives (but not false negatives), since + // some of the `Settings` in the path may ultimately be unused (or, e.g., they + // could have their `required_version` overridden by other `Settings` in + // the path). It'd be preferable to validate once we've determined the + // `Settings` for each path, but that's more expensive. + match &strategy { + PyprojectDiscovery::Fixed(settings) => { + settings.validate()?; + } + PyprojectDiscovery::Hierarchical(default) => { + for settings in std::iter::once(default).chain(self.iter()) { + settings.validate()?; + } + } + } + Ok(()) + } } /// Recursively resolve a `Configuration` from a `pyproject.toml` file at the diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 632d161588a5fe..4e98940d9183cc 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -16,7 +16,9 @@ use crate::checks_gen::CheckCodePrefix; use crate::cli::{collect_per_file_ignores, Overrides}; use crate::settings::options::Options; use crate::settings::pyproject::load_options; -use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; +use crate::settings::types::{ + FilePattern, PerFileIgnore, PythonVersion, SerializationFormat, Version, +}; use crate::{ flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes, flake8_tidy_imports, flake8_unused_arguments, fs, isort, mccabe, pep8_naming, pyupgrade, @@ -40,6 +42,7 @@ pub struct Configuration { pub ignore_init_module_imports: Option, pub line_length: Option, pub per_file_ignores: Option>, + pub required_version: Option, pub respect_gitignore: Option, pub select: Option>, pub show_source: Option, @@ -122,6 +125,7 @@ impl Configuration { }) .collect() }), + required_version: options.required_version, respect_gitignore: options.respect_gitignore, select: options.select, show_source: options.show_source, @@ -160,7 +164,6 @@ impl Configuration { allowed_confusables: self.allowed_confusables.or(config.allowed_confusables), dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx), exclude: self.exclude.or(config.exclude), - respect_gitignore: self.respect_gitignore.or(config.respect_gitignore), extend: self.extend.or(config.extend), extend_exclude: config .extend_exclude @@ -188,6 +191,8 @@ impl Configuration { .or(config.ignore_init_module_imports), line_length: self.line_length.or(config.line_length), per_file_ignores: self.per_file_ignores.or(config.per_file_ignores), + required_version: self.required_version.or(config.required_version), + respect_gitignore: self.respect_gitignore.or(config.respect_gitignore), select: self.select.or(config.select), show_source: self.show_source.or(config.show_source), src: self.src.or(config.src), diff --git a/src/settings/mod.rs b/src/settings/mod.rs index de360546b7c4bb..8c24f38091fe35 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -5,7 +5,7 @@ use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use globset::{Glob, GlobMatcher, GlobSet}; use itertools::Itertools; use once_cell::sync::Lazy; @@ -17,7 +17,9 @@ use crate::cache::cache_dir; use crate::checks::CheckCode; use crate::checks_gen::{CheckCodePrefix, SuffixLength, CATEGORIES}; use crate::settings::configuration::Configuration; -use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; +use crate::settings::types::{ + FilePattern, PerFileIgnore, PythonVersion, SerializationFormat, Version, +}; use crate::{ flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes, flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pyupgrade, @@ -30,6 +32,8 @@ pub mod options_base; pub mod pyproject; pub mod types; +const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + #[derive(Debug)] #[allow(clippy::struct_excessive_bools)] pub struct Settings { @@ -46,6 +50,7 @@ pub struct Settings { pub ignore_init_module_imports: bool, pub line_length: usize, pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet)>, + pub required_version: Option, pub respect_gitignore: bool, pub show_source: bool, pub src: Vec, @@ -137,6 +142,7 @@ impl Settings { config.per_file_ignores.unwrap_or_default(), )?, respect_gitignore: config.respect_gitignore.unwrap_or(true), + required_version: config.required_version, src: config .src .unwrap_or_else(|| vec![project_root.to_path_buf()]), @@ -208,6 +214,7 @@ impl Settings { ignore_init_module_imports: false, line_length: 88, per_file_ignores: vec![], + required_version: None, respect_gitignore: true, show_source: false, src: vec![path_dedot::CWD.clone()], @@ -242,6 +249,7 @@ impl Settings { ignore_init_module_imports: false, line_length: 88, per_file_ignores: vec![], + required_version: None, respect_gitignore: true, show_source: false, src: vec![path_dedot::CWD.clone()], @@ -260,6 +268,19 @@ impl Settings { pyupgrade: pyupgrade::settings::Settings::default(), } } + + pub fn validate(&self) -> Result<()> { + if let Some(required_version) = &self.required_version { + if &**required_version != CARGO_PKG_VERSION { + return Err(anyhow!( + "Required version `{}` does not match the running version `{}`", + &**required_version, + CARGO_PKG_VERSION + )); + } + } + Ok(()) + } } impl Hash for Settings { diff --git a/src/settings/options.rs b/src/settings/options.rs index 5fc9d4333835dc..7dd10dab2814c8 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::checks_gen::CheckCodePrefix; -use crate::settings::types::{PythonVersion, SerializationFormat}; +use crate::settings::types::{PythonVersion, SerializationFormat, Version}; use crate::{ flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes, flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pyupgrade, @@ -213,6 +213,17 @@ pub struct Options { /// The line length to use when enforcing long-lines violations (like /// `E501`). pub line_length: Option, + #[option( + default = "None", + value_type = "String", + example = r#" + required-version = "0.0.193" + "# + )] + /// Require a specific version of Ruff to be running (useful for unifying + /// results across many environments, e.g., with a `pyproject.toml` + /// file). + pub required_version: Option, #[option( default = "true", value_type = "bool", diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index e7321ee9b530db..f5913c1c274403 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -136,6 +136,7 @@ mod tests { line_length: None, per_file_ignores: None, respect_gitignore: None, + required_version: None, select: None, show_source: None, src: None, @@ -185,6 +186,7 @@ line-length = 79 line_length: Some(79), per_file_ignores: None, respect_gitignore: None, + required_version: None, select: None, show_source: None, src: None, @@ -234,6 +236,7 @@ exclude = ["foo.py"] line_length: None, per_file_ignores: None, respect_gitignore: None, + required_version: None, select: None, show_source: None, src: None, @@ -283,6 +286,7 @@ select = ["E501"] line_length: None, per_file_ignores: None, respect_gitignore: None, + required_version: None, select: Some(vec![CheckCodePrefix::E501]), show_source: None, src: None, @@ -333,6 +337,7 @@ ignore = ["E501"] line_length: None, per_file_ignores: None, respect_gitignore: None, + required_version: None, select: None, show_source: None, src: None, @@ -427,6 +432,7 @@ other-attribute = 1 )])), dummy_variable_rgx: None, respect_gitignore: None, + required_version: None, src: None, target_version: None, show_source: None, diff --git a/src/settings/types.rs b/src/settings/types.rs index 25aefce70ef6ee..427926a5d458bf 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -1,5 +1,6 @@ use std::env; use std::hash::Hash; +use std::ops::Deref; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -165,3 +166,23 @@ impl Default for SerializationFormat { Self::Text } } + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(try_from = "String")] +pub struct Version(String); + +impl TryFrom for Version { + type Error = semver::Error; + + fn try_from(value: String) -> Result { + semver::Version::parse(&value).map(|_| Self(value)) + } +} + +impl Deref for Version { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +}