diff --git a/flake8_to_ruff/examples/cryptography/pyproject.toml b/flake8_to_ruff/examples/cryptography/pyproject.toml new file mode 100644 index 0000000000..0fa34fb55a --- /dev/null +++ b/flake8_to_ruff/examples/cryptography/pyproject.toml @@ -0,0 +1,65 @@ +[build-system] +requires = [ + # The minimum setuptools version is specific to the PEP 517 backend, + # and may be stricter than the version required in `setup.cfg` + "setuptools>=40.6.0,!=60.9.0", + "wheel", + # Must be kept in sync with the `install_requirements` in `setup.cfg` + "cffi>=1.12; platform_python_implementation != 'PyPy'", + "setuptools-rust>=0.11.4", +] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 79 +target-version = ["py36"] + +[tool.pytest.ini_options] +addopts = "-r s --capture=no --strict-markers --benchmark-disable" +markers = [ + "skip_fips: this test is not executed in FIPS mode", + "supported: parametrized test requiring only_if and skip_message", +] + +[tool.mypy] +show_error_codes = true +check_untyped_defs = true +no_implicit_reexport = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_unused_configs = true +strict_equality = true + +[[tool.mypy.overrides]] +module = [ + "pretend" +] +ignore_missing_imports = true + +[tool.coverage.run] +branch = true +relative_files = true +source = [ + "cryptography", + "tests/", +] + +[tool.coverage.paths] +source = [ + "src/cryptography", + "*.tox/*/lib*/python*/site-packages/cryptography", + "*.tox\\*\\Lib\\site-packages\\cryptography", + "*.tox/pypy/site-packages/cryptography", +] +tests =[ + "tests/", + "*tests\\", +] + +[tool.coverage.report] +exclude_lines = [ + "@abc.abstractmethod", + "@abc.abstractproperty", + "@typing.overload", + "if typing.TYPE_CHECKING", +] diff --git a/flake8_to_ruff/examples/cryptography/setup.cfg b/flake8_to_ruff/examples/cryptography/setup.cfg new file mode 100644 index 0000000000..d42e78930c --- /dev/null +++ b/flake8_to_ruff/examples/cryptography/setup.cfg @@ -0,0 +1,91 @@ +[metadata] +name = cryptography +version = attr: cryptography.__version__ +description = cryptography is a package which provides cryptographic recipes and primitives to Python developers. +long_description = file: README.rst +long_description_content_type = text/x-rst +license = BSD-3-Clause OR Apache-2.0 +url = https://github.com/pyca/cryptography +author = The Python Cryptographic Authority and individual contributors +author_email = cryptography-dev@python.org +project_urls = + Documentation=https://cryptography.io/ + Source=https://github.com/pyca/cryptography/ + Issues=https://github.com/pyca/cryptography/issues + Changelog=https://cryptography.io/en/latest/changelog/ +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + License :: OSI Approved :: BSD License + Natural Language :: English + Operating System :: MacOS :: MacOS X + Operating System :: POSIX + Operating System :: POSIX :: BSD + Operating System :: POSIX :: Linux + Operating System :: Microsoft :: Windows + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Security :: Cryptography + +[options] +python_requires = >=3.6 +include_package_data = True +zip_safe = False +package_dir = + =src +packages = find: +# `install_requires` must be kept in sync with `pyproject.toml` +install_requires = + cffi >=1.12 + +[options.packages.find] +where = src +exclude = + _cffi_src + _cffi_src.* + +[options.extras_require] +test = + pytest>=6.2.0 + pytest-benchmark + pytest-cov + pytest-subtests + pytest-xdist + pretend + iso8601 + pytz + hypothesis>=1.11.4,!=3.79.2 +docs = + sphinx >= 1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0 + sphinx_rtd_theme +docstest = + pyenchant >= 1.6.11 + twine >= 1.12.0 + sphinxcontrib-spelling >= 4.0.1 +sdist = + setuptools_rust >= 0.11.4 +pep8test = + black + flake8 + flake8-import-order + pep8-naming +# This extra is for OpenSSH private keys that use bcrypt KDF +# Versions: v3.1.3 - ignore_few_rounds, v3.1.5 - abi3 +ssh = + bcrypt >= 3.1.5 + +[flake8] +ignore = E203,E211,W503,W504,N818 +exclude = .tox,*.egg,.git,_build,.hypothesis +select = E,W,F,N,I +application-import-names = cryptography,cryptography_vectors,tests diff --git a/flake8_to_ruff/src/black.rs b/flake8_to_ruff/src/black.rs new file mode 100644 index 0000000000..68e668d6a6 --- /dev/null +++ b/flake8_to_ruff/src/black.rs @@ -0,0 +1,32 @@ +//! Extract Black configuration settings from a pyproject.toml. + +use std::path::Path; + +use anyhow::Result; +use ruff::settings::types::PythonVersion; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Black { + #[serde(alias = "line-length", alias = "line_length")] + pub line_length: Option, + #[serde(alias = "target-version", alias = "target_version")] + pub target_version: Option>, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Tools { + black: Option, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Pyproject { + tool: Option, +} + +pub fn parse_black_options>(path: P) -> Result> { + let contents = std::fs::read_to_string(path)?; + Ok(toml::from_str::(&contents)? + .tool + .and_then(|tool| tool.black)) +} diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index c79676d94f..1547bd7f0f 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -11,13 +11,20 @@ use ruff::{ pep8_naming, }; +use crate::black::Black; use crate::plugin::Plugin; use crate::{parser, plugin}; pub fn convert( - flake8: &HashMap>, + config: &HashMap>>, + black: Option<&Black>, plugins: Option>, ) -> Result { + // Extract the Flake8 section. + let flake8 = config + .get("flake8") + .expect("Unable to find flake8 section in INI file"); + // Extract all referenced check code prefixes, to power plugin inference. let mut referenced_codes: BTreeSet = BTreeSet::default(); for (key, value) in flake8 { @@ -236,6 +243,19 @@ pub fn convert( options.pep8_naming = Some(pep8_naming); } + // Extract any settings from the existing `pyproject.toml`. + if let Some(black) = black { + if let Some(line_length) = &black.line_length { + options.line_length = Some(*line_length); + } + + if let Some(target_version) = &black.target_version { + if let Some(target_version) = target_version.iter().min() { + options.target_version = Some(*target_version); + } + } + } + // Create the pyproject.toml. Ok(Pyproject::new(options)) } @@ -255,7 +275,7 @@ mod tests { #[test] fn it_converts_empty() -> Result<()> { - let actual = convert(&HashMap::from([]), None)?; + let actual = convert(&HashMap::from([]), None, None)?; let expected = Pyproject::new(Options { allowed_confusables: None, dummy_variable_rgx: None, @@ -303,7 +323,11 @@ mod tests { #[test] fn it_converts_dashes() -> Result<()> { let actual = convert( - &HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]), + &HashMap::from([( + "flake8".to_string(), + HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]), + )]), + None, Some(vec![]), )?; let expected = Pyproject::new(Options { @@ -353,7 +377,11 @@ mod tests { #[test] fn it_converts_underscores() -> Result<()> { let actual = convert( - &HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]), + &HashMap::from([( + "flake8".to_string(), + HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]), + )]), + None, Some(vec![]), )?; let expected = Pyproject::new(Options { @@ -403,7 +431,11 @@ mod tests { #[test] fn it_ignores_parse_errors() -> Result<()> { let actual = convert( - &HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]), + &HashMap::from([( + "flake8".to_string(), + HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]), + )]), + None, Some(vec![]), )?; let expected = Pyproject::new(Options { @@ -453,7 +485,11 @@ mod tests { #[test] fn it_converts_plugin_options() -> Result<()> { let actual = convert( - &HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]), + &HashMap::from([( + "flake8".to_string(), + HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]), + )]), + None, Some(vec![]), )?; let expected = Pyproject::new(Options { @@ -509,9 +545,13 @@ mod tests { fn it_converts_docstring_conventions() -> Result<()> { let actual = convert( &HashMap::from([( - "docstring-convention".to_string(), - Some("numpy".to_string()), + "flake8".to_string(), + HashMap::from([( + "docstring-convention".to_string(), + Some("numpy".to_string()), + )]), )]), + None, Some(vec![Plugin::Flake8Docstrings]), )?; let expected = Pyproject::new(Options { @@ -597,7 +637,11 @@ mod tests { #[test] fn it_infers_plugins_if_omitted() -> Result<()> { let actual = convert( - &HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]), + &HashMap::from([( + "flake8".to_string(), + HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]), + )]), + None, None, )?; let expected = Pyproject::new(Options { diff --git a/flake8_to_ruff/src/lib.rs b/flake8_to_ruff/src/lib.rs index a13d73a2df..157350ab2c 100644 --- a/flake8_to_ruff/src/lib.rs +++ b/flake8_to_ruff/src/lib.rs @@ -11,6 +11,7 @@ clippy::too_many_lines )] +pub mod black; pub mod converter; mod parser; pub mod plugin; diff --git a/flake8_to_ruff/src/main.rs b/flake8_to_ruff/src/main.rs index cada6227ff..bcc069caef 100644 --- a/flake8_to_ruff/src/main.rs +++ b/flake8_to_ruff/src/main.rs @@ -17,6 +17,7 @@ use std::path::PathBuf; use anyhow::Result; use clap::Parser; use configparser::ini::Ini; +use flake8_to_ruff::black::parse_black_options; use flake8_to_ruff::converter; use flake8_to_ruff::plugin::Plugin; @@ -26,10 +27,14 @@ use flake8_to_ruff::plugin::Plugin; long_about = None )] struct Cli { - /// Path to the Flake8 configuration file (e.g., 'setup.cfg', 'tox.ini', or - /// '.flake8'). + /// Path to the Flake8 configuration file (e.g., `setup.cfg`, `tox.ini`, or + /// `.flake8`). #[arg(required = true)] file: PathBuf, + /// Optional path to a `pyproject.toml` file, used to ensure compatibility + /// with Black. + #[arg(long)] + pyproject: Option, /// List of plugins to enable. #[arg(long, value_delimiter = ',')] plugin: Option>, @@ -43,13 +48,15 @@ fn main() -> Result<()> { ini.set_multiline(true); let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?; - // Extract the Flake8 section. - let flake8 = config - .get("flake8") - .expect("Unable to find flake8 section in INI file"); + // Read the pyproject.toml file. + let black = cli + .pyproject + .map(parse_black_options) + .transpose()? + .flatten(); - // Create the pyproject.toml. - let pyproject = converter::convert(flake8, cli.plugin)?; + // Create Ruff's pyproject.toml section. + let pyproject = converter::convert(&config, black.as_ref(), cli.plugin)?; println!("{}", toml::to_string_pretty(&pyproject)?); Ok(()) diff --git a/src/settings/types.rs b/src/settings/types.rs index c18ff77d8a..095e80d5ca 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -13,7 +13,7 @@ use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix; use crate::fs; -#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "lowercase")] pub enum PythonVersion { Py33,