Add --required-version (#1376)

This commit is contained in:
Charlie Marsh 2022-12-25 19:53:50 -05:00 committed by GitHub
parent 19121219fb
commit 8b72f55a09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 183 additions and 17 deletions

5
Cargo.lock generated
View file

@ -1902,6 +1902,7 @@ dependencies = [
"rustpython-common", "rustpython-common",
"rustpython-parser", "rustpython-parser",
"schemars", "schemars",
"semver",
"serde", "serde",
"serde_json", "serde_json",
"shellexpand", "shellexpand",
@ -2122,9 +2123,9 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.14" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]] [[package]]
name = "serde" name = "serde"

View file

@ -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-common = { git = "https://github.com/RustPython/RustPython.git", rev = "1b6cb170e925a43d605b3fed9f6b878e63e47744" }
rustpython-parser = { features = ["lalrpop"], 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" } schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" } serde_json = { version = "1.0.87" }
shellexpand = { version = "3.0.0" } shellexpand = { version = "3.0.0" }

View file

@ -1965,6 +1965,25 @@ 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) #### [`respect-gitignore`](#respect-gitignore)
Whether to automatically exclude files that are ignored by `.ignore`, Whether to automatically exclude files that are ignored by `.ignore`,

View file

@ -303,6 +303,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::E, CheckCodePrefix::E,
@ -359,6 +360,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: Some(100), line_length: Some(100),
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::E, CheckCodePrefix::E,
@ -415,6 +417,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: Some(100), line_length: Some(100),
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::E, CheckCodePrefix::E,
@ -471,6 +474,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::E, CheckCodePrefix::E,
@ -527,6 +531,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::E, CheckCodePrefix::E,
@ -591,6 +596,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::D100, CheckCodePrefix::D100,
@ -683,6 +689,7 @@ mod tests {
ignore_init_module_imports: None, ignore_init_module_imports: None,
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
required_version: None,
respect_gitignore: None, respect_gitignore: None,
select: Some(vec![ select: Some(vec![
CheckCodePrefix::E, CheckCodePrefix::E,

View file

@ -34,7 +34,11 @@ bindings = "bin"
strip = true strip = true
[tool.ruff] [tool.ruff]
#required-version = "0.0.192"
[tool.ruff.isort] [tool.ruff.isort]
force-wrap-aliases = true force-wrap-aliases = true
combine-as-imports = true combine-as-imports = true
[tool.black]
required-version = "22.12.1"

View file

@ -288,6 +288,17 @@
} }
] ]
}, },
"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).",
"anyOf": [
{
"$ref": "#/definitions/Version"
},
{
"type": "null"
}
]
},
"respect-gitignore": { "respect-gitignore": {
"description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.", "description": "Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`, `.git/info/exclude`, and global `gitignore` files. Enabled by default.",
"type": [ "type": [
@ -1196,6 +1207,9 @@
"parents", "parents",
"all" "all"
] ]
},
"Version": {
"type": "string"
} }
} }
} }

View file

@ -38,14 +38,8 @@ pub fn run(
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
// Discover the package root for each Python file. // Validate the `Settings` and return any errors.
let package_roots = packages::detect_package_roots( resolver.validate(pyproject_strategy)?;
&paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.collect::<Vec<_>>(),
);
// Initialize the cache. // Initialize the cache.
if matches!(cache, flags::Cache::Enabled) { 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::<Vec<_>>(),
);
let start = Instant::now(); let start = Instant::now();
let mut diagnostics: Diagnostics = par_iter(&paths) let mut diagnostics: Diagnostics = par_iter(&paths)
.map(|entry| { .map(|entry| {
@ -176,6 +179,9 @@ pub fn add_noqa(
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let start = Instant::now(); let start = Instant::now();
let modifications: usize = par_iter(&paths) let modifications: usize = par_iter(&paths)
.flatten() .flatten()
@ -212,6 +218,9 @@ pub fn autoformat(
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let start = Instant::now(); let start = Instant::now();
let modifications = par_iter(&paths) let modifications = par_iter(&paths)
.flatten() .flatten()
@ -245,6 +254,9 @@ pub fn show_settings(
let (paths, resolver) = let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?; 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. // Print the list of files.
let Some(entry) = paths let Some(entry) = paths
.iter() .iter()
@ -268,9 +280,12 @@ pub fn show_files(
overrides: &Overrides, overrides: &Overrides,
) -> Result<()> { ) -> Result<()> {
// Collect all files in the hierarchy. // Collect all files in the hierarchy.
let (paths, _resolver) = let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?; 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. // Print the list of files.
for entry in paths for entry in paths
.iter() .iter()

View file

@ -59,6 +59,9 @@ pub(crate) fn check_path(
autofix: flags::Autofix, autofix: flags::Autofix,
noqa: flags::Noqa, noqa: flags::Noqa,
) -> Result<Vec<Check>> { ) -> Result<Vec<Check>> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Aggregate all checks. // Aggregate all checks.
let mut checks: Vec<Check> = vec![]; let mut checks: Vec<Check> = vec![];
@ -175,6 +178,9 @@ pub fn lint_path(
cache: flags::Cache, cache: flags::Cache,
autofix: fixer::Mode, autofix: fixer::Mode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.validate()?;
let metadata = path.metadata()?; let metadata = path.metadata()?;
// Check the cache. // Check the cache.
@ -202,6 +208,9 @@ pub fn lint_path(
/// Add any missing `#noqa` pragmas to the source code at the given `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<usize> { pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Read the file from disk. // Read the file from disk.
let contents = fs::read_file(path)?; let contents = fs::read_file(path)?;
@ -241,7 +250,10 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
} }
/// Apply autoformatting to the source code at the given `Path`. /// 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. // Read the file from disk.
let contents = fs::read_file(path)?; let contents = fs::read_file(path)?;
@ -266,6 +278,9 @@ pub fn lint_stdin(
settings: &Settings, settings: &Settings,
autofix: fixer::Mode, autofix: fixer::Mode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Read the file from disk. // Read the file from disk.
let contents = stdin.to_string(); let contents = stdin.to_string();

View file

@ -100,6 +100,12 @@ fn inner_main() -> Result<ExitCode> {
cli.stdin_filename.as_deref(), 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 // Extract options that are included in `Settings`, but only apply at the top
// level. // level.
let file_strategy = FileDiscovery { let file_strategy = FileDiscovery {

View file

@ -91,6 +91,26 @@ impl Resolver {
pub fn iter(&self) -> impl Iterator<Item = &Settings> { pub fn iter(&self) -> impl Iterator<Item = &Settings> {
self.settings.values() 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 /// Recursively resolve a `Configuration` from a `pyproject.toml` file at the

View file

@ -16,7 +16,9 @@ use crate::checks_gen::CheckCodePrefix;
use crate::cli::{collect_per_file_ignores, Overrides}; use crate::cli::{collect_per_file_ignores, Overrides};
use crate::settings::options::Options; use crate::settings::options::Options;
use crate::settings::pyproject::load_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::{ use crate::{
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes, flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
flake8_tidy_imports, flake8_unused_arguments, fs, isort, mccabe, pep8_naming, pyupgrade, flake8_tidy_imports, flake8_unused_arguments, fs, isort, mccabe, pep8_naming, pyupgrade,
@ -41,6 +43,7 @@ pub struct Configuration {
pub ignore_init_module_imports: Option<bool>, pub ignore_init_module_imports: Option<bool>,
pub line_length: Option<usize>, pub line_length: Option<usize>,
pub per_file_ignores: Option<Vec<PerFileIgnore>>, pub per_file_ignores: Option<Vec<PerFileIgnore>>,
pub required_version: Option<Version>,
pub respect_gitignore: Option<bool>, pub respect_gitignore: Option<bool>,
pub select: Option<Vec<CheckCodePrefix>>, pub select: Option<Vec<CheckCodePrefix>>,
pub show_source: Option<bool>, pub show_source: Option<bool>,
@ -124,6 +127,7 @@ impl Configuration {
}) })
.collect() .collect()
}), }),
required_version: options.required_version,
respect_gitignore: options.respect_gitignore, respect_gitignore: options.respect_gitignore,
select: options.select, select: options.select,
show_source: options.show_source, show_source: options.show_source,
@ -162,7 +166,6 @@ impl Configuration {
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables), allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx), dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
exclude: self.exclude.or(config.exclude), exclude: self.exclude.or(config.exclude),
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
extend: self.extend.or(config.extend), extend: self.extend.or(config.extend),
extend_exclude: config extend_exclude: config
.extend_exclude .extend_exclude
@ -191,6 +194,8 @@ impl Configuration {
.or(config.ignore_init_module_imports), .or(config.ignore_init_module_imports),
line_length: self.line_length.or(config.line_length), line_length: self.line_length.or(config.line_length),
per_file_ignores: self.per_file_ignores.or(config.per_file_ignores), 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), select: self.select.or(config.select),
show_source: self.show_source.or(config.show_source), show_source: self.show_source.or(config.show_source),
src: self.src.or(config.src), src: self.src.or(config.src),

View file

@ -5,7 +5,7 @@
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::{anyhow, Result};
use globset::{Glob, GlobMatcher, GlobSet}; use globset::{Glob, GlobMatcher, GlobSet};
use itertools::Itertools; use itertools::Itertools;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -17,7 +17,9 @@ use crate::cache::cache_dir;
use crate::checks::CheckCode; use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, SuffixLength, CATEGORIES}; use crate::checks_gen::{CheckCodePrefix, SuffixLength, CATEGORIES};
use crate::settings::configuration::Configuration; use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat}; use crate::settings::types::{
FilePattern, PerFileIgnore, PythonVersion, SerializationFormat, Version,
};
use crate::{ use crate::{
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes, flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pyupgrade, flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pyupgrade,
@ -30,6 +32,8 @@ pub mod options_base;
pub mod pyproject; pub mod pyproject;
pub mod types; pub mod types;
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct Settings { pub struct Settings {
@ -47,6 +51,7 @@ pub struct Settings {
pub ignore_init_module_imports: bool, pub ignore_init_module_imports: bool,
pub line_length: usize, pub line_length: usize,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>, pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>,
pub required_version: Option<Version>,
pub respect_gitignore: bool, pub respect_gitignore: bool,
pub show_source: bool, pub show_source: bool,
pub src: Vec<PathBuf>, pub src: Vec<PathBuf>,
@ -139,6 +144,7 @@ impl Settings {
config.per_file_ignores.unwrap_or_default(), config.per_file_ignores.unwrap_or_default(),
)?, )?,
respect_gitignore: config.respect_gitignore.unwrap_or(true), respect_gitignore: config.respect_gitignore.unwrap_or(true),
required_version: config.required_version,
src: config src: config
.src .src
.unwrap_or_else(|| vec![project_root.to_path_buf()]), .unwrap_or_else(|| vec![project_root.to_path_buf()]),
@ -211,6 +217,7 @@ impl Settings {
ignore_init_module_imports: false, ignore_init_module_imports: false,
line_length: 88, line_length: 88,
per_file_ignores: vec![], per_file_ignores: vec![],
required_version: None,
respect_gitignore: true, respect_gitignore: true,
show_source: false, show_source: false,
src: vec![path_dedot::CWD.clone()], src: vec![path_dedot::CWD.clone()],
@ -246,6 +253,7 @@ impl Settings {
ignore_init_module_imports: false, ignore_init_module_imports: false,
line_length: 88, line_length: 88,
per_file_ignores: vec![], per_file_ignores: vec![],
required_version: None,
respect_gitignore: true, respect_gitignore: true,
show_source: false, show_source: false,
src: vec![path_dedot::CWD.clone()], src: vec![path_dedot::CWD.clone()],
@ -264,6 +272,19 @@ impl Settings {
pyupgrade: pyupgrade::settings::Settings::default(), 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 { impl Hash for Settings {

View file

@ -6,7 +6,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::checks_gen::CheckCodePrefix; use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::{PythonVersion, SerializationFormat}; use crate::settings::types::{PythonVersion, SerializationFormat, Version};
use crate::{ use crate::{
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes, flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions, flake8_quotes,
flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pyupgrade, flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pyupgrade,
@ -216,6 +216,17 @@ pub struct Options {
/// The line length to use when enforcing long-lines violations (like /// The line length to use when enforcing long-lines violations (like
/// `E501`). /// `E501`).
pub line_length: Option<usize>, pub line_length: Option<usize>,
#[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<Version>,
#[option( #[option(
default = "true", default = "true",
value_type = "bool", value_type = "bool",

View file

@ -137,6 +137,7 @@ mod tests {
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None,
select: None, select: None,
show_source: None, show_source: None,
src: None, src: None,
@ -187,6 +188,7 @@ line-length = 79
line_length: Some(79), line_length: Some(79),
per_file_ignores: None, per_file_ignores: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None,
select: None, select: None,
show_source: None, show_source: None,
src: None, src: None,
@ -237,6 +239,7 @@ exclude = ["foo.py"]
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None,
select: None, select: None,
show_source: None, show_source: None,
src: None, src: None,
@ -287,6 +290,7 @@ select = ["E501"]
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None,
select: Some(vec![CheckCodePrefix::E501]), select: Some(vec![CheckCodePrefix::E501]),
show_source: None, show_source: None,
src: None, src: None,
@ -338,6 +342,7 @@ ignore = ["E501"]
line_length: None, line_length: None,
per_file_ignores: None, per_file_ignores: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None,
select: None, select: None,
show_source: None, show_source: None,
src: None, src: None,
@ -433,6 +438,7 @@ other-attribute = 1
)])), )])),
dummy_variable_rgx: None, dummy_variable_rgx: None,
respect_gitignore: None, respect_gitignore: None,
required_version: None,
src: None, src: None,
target_version: None, target_version: None,
show_source: None, show_source: None,

View file

@ -1,5 +1,6 @@
use std::env; use std::env;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
@ -165,3 +166,23 @@ impl Default for SerializationFormat {
Self::Text Self::Text
} }
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(try_from = "String")]
pub struct Version(String);
impl TryFrom<String> for Version {
type Error = semver::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
semver::Version::parse(&value).map(|_| Self(value))
}
}
impl Deref for Version {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}