Skip to content

Commit

Permalink
Infer target-version from project metadata (#3470)
Browse files Browse the repository at this point in the history
* Infer target-version from project metadata

* Fix requires-python with ">=3.8.16"

* Load requires-python at runtime

* Use upstream VersionSpecifiers

* Add debug information when parsing ruff.toml

* Display debug only if target_version is not set

* Bump pep440-rs to add impl Error for Pep440Error
  • Loading branch information
JonathanPlasse authored Mar 13, 2023
1 parent 3a5fbd6 commit b540407
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 6 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions crates/flake8_to_ruff/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,15 @@ fn main() -> Result<()> {
.map(|tool| ExternalConfig {
black: tool.black.as_ref(),
isort: tool.isort.as_ref(),
..Default::default()
})
.unwrap_or_default();
let external_config = ExternalConfig {
project: pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
..external_config
};

// Create Ruff's pyproject.toml section.
let pyproject = flake8_to_ruff::convert(&config, &external_config, args.plugin)?;
Expand Down
6 changes: 5 additions & 1 deletion crates/ruff/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ path-absolutize = { workspace = true, features = [
"use_unix_paths_on_wasm",
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { git = "https://github.com/konstin/pep440-rs.git", features = [
"serde",
], rev = "a8fef4ec47f4c25b070b39cdbe6a0b9847e49941" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
rustc-hash = { workspace = true }
Expand All @@ -64,9 +67,10 @@ thiserror = { version = "1.0.38" }
toml = { workspace = true }

[dev-dependencies]
criterion = { version = "0.4.0" }
insta = { workspace = true, features = ["yaml", "redactions"] }
pretty_assertions = "1.3.0"
test-case = { workspace = true }
criterion = { version = "0.4.0" }


[features]
Expand Down
36 changes: 36 additions & 0 deletions crates/ruff/src/flake8_to_ruff/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::rules::{
};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;
use crate::warn_user;

const DEFAULT_SELECTORS: &[RuleSelector] = &[
Expand Down Expand Up @@ -424,6 +425,15 @@ pub fn convert(
}
}

if let Some(project) = &external_config.project {
if let Some(requires_python) = &project.requires_python {
if options.target_version.is_none() {
options.target_version =
PythonVersion::get_minimum_supported_version(requires_python);
}
}
}

// Create the pyproject.toml.
Ok(Pyproject::new(options))
}
Expand All @@ -439,20 +449,25 @@ fn resolve_select(plugins: &[Plugin]) -> HashSet<RuleSelector> {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::str::FromStr;

use anyhow::Result;
use itertools::Itertools;
use pep440_rs::VersionSpecifiers;
use pretty_assertions::assert_eq;

use super::super::plugin::Plugin;
use super::convert;
use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS;
use crate::flake8_to_ruff::pep621::Project;
use crate::flake8_to_ruff::ExternalConfig;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
use crate::settings::types::PythonVersion;

fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options {
Options {
Expand Down Expand Up @@ -609,4 +624,25 @@ mod tests {

Ok(())
}

#[test]
fn it_converts_project_requires_python() -> Result<()> {
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
&ExternalConfig {
project: Some(&Project {
requires_python: Some(VersionSpecifiers::from_str(">=3.8.16, <3.11")?),
}),
..ExternalConfig::default()
},
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
target_version: Some(PythonVersion::Py38),
..default_options([])
});
assert_eq!(actual, expected);

Ok(())
}
}
2 changes: 2 additions & 0 deletions crates/ruff/src/flake8_to_ruff/external_config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use super::black::Black;
use super::isort::Isort;
use super::pep621::Project;

#[derive(Default)]
pub struct ExternalConfig<'a> {
pub black: Option<&'a Black>,
pub isort: Option<&'a Isort>,
pub project: Option<&'a Project>,
}
1 change: 1 addition & 0 deletions crates/ruff/src/flake8_to_ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod converter;
mod external_config;
mod isort;
mod parser;
pub mod pep621;
mod plugin;
mod pyproject;

Expand Down
10 changes: 10 additions & 0 deletions crates/ruff/src/flake8_to_ruff/pep621.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Extract PEP 621 configuration settings from a pyproject.toml.
use pep440_rs::VersionSpecifiers;
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Project {
#[serde(alias = "requires-python", alias = "requires_python")]
pub requires_python: Option<VersionSpecifiers>,
}
2 changes: 2 additions & 0 deletions crates/ruff/src/flake8_to_ruff/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};

use super::black::Black;
use super::isort::Isort;
use super::pep621::Project;

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tools {
Expand All @@ -15,6 +16,7 @@ pub struct Tools {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pyproject {
pub tool: Option<Tools>,
pub project: Option<Project>,
}

pub fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
Expand Down
28 changes: 24 additions & 4 deletions crates/ruff/src/settings/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
use std::path::{Path, PathBuf};

use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize};

use crate::flake8_to_ruff::pep621::Project;
use crate::settings::options::Options;
use crate::settings::types::PythonVersion;

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools {
ruff: Option<Options>,
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Pyproject {
tool: Option<Tools>,
project: Option<Project>,
}

impl Pyproject {
Expand All @@ -23,6 +27,7 @@ impl Pyproject {
tool: Some(Tools {
ruff: Some(options),
}),
project: None,
}
}
}
Expand Down Expand Up @@ -114,12 +119,27 @@ pub fn find_user_settings_toml() -> Option<PathBuf> {
pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
if path.as_ref().ends_with("pyproject.toml") {
let pyproject = parse_pyproject_toml(&path)?;
Ok(pyproject
let mut ruff = pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default())
.unwrap_or_default();
if ruff.target_version.is_none() {
if let Some(project) = pyproject.project {
if let Some(requires_python) = project.requires_python {
ruff.target_version =
PythonVersion::get_minimum_supported_version(&requires_python);
}
}
}
Ok(ruff)
} else {
parse_ruff_toml(path)
let ruff = parse_ruff_toml(path);
if let Ok(ruff) = &ruff {
if ruff.target_version.is_none() {
debug!("`project.requires_python` in `pyproject.toml` will not be used to set `target_version` when using `ruff.toml`.");
}
}
ruff
}
}

Expand Down
38 changes: 37 additions & 1 deletion crates/ruff/src/settings/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,37 @@ use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;

use anyhow::{anyhow, bail, Result};
use clap::ValueEnum;
use globset::{Glob, GlobSet, GlobSetBuilder};
use pep440_rs::{Version as Pep440Version, VersionSpecifiers};
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_macros::CacheKey;
use rustc_hash::FxHashSet;
use schemars::JsonSchema;
use serde::{de, Deserialize, Deserializer, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

use crate::registry::Rule;
use crate::rule_selector::RuleSelector;
use crate::{fs, warn_user_once};

#[derive(
Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, JsonSchema, CacheKey,
Clone,
Copy,
Debug,
PartialOrd,
Ord,
PartialEq,
Eq,
Serialize,
Deserialize,
JsonSchema,
CacheKey,
EnumIter,
)]
#[serde(rename_all = "lowercase")]
pub enum PythonVersion {
Expand Down Expand Up @@ -50,6 +65,13 @@ impl FromStr for PythonVersion {
}
}

impl From<PythonVersion> for Pep440Version {
fn from(version: PythonVersion) -> Self {
let (major, minor) = version.as_tuple();
Self::from_str(&format!("{major}.{minor}.100")).unwrap()
}
}

impl PythonVersion {
pub const fn as_tuple(&self) -> (u32, u32) {
match self {
Expand All @@ -60,6 +82,20 @@ impl PythonVersion {
Self::Py311 => (3, 11),
}
}

pub fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<Self> {
let mut minimum_version = None;
for python_version in PythonVersion::iter() {
if requires_version
.iter()
.all(|specifier| specifier.contains(&python_version.into()))
{
minimum_version = Some(python_version);
break;
}
}
minimum_version
}
}

#[derive(Debug, Clone, CacheKey, PartialEq, PartialOrd, Eq, Ord)]
Expand Down

0 comments on commit b540407

Please sign in to comment.