From 76f8e5b7558a9104cadcbc2cab864a6350bbea8b Mon Sep 17 00:00:00 2001 From: Renkai Ge Date: Tue, 7 Oct 2025 17:28:00 +0800 Subject: [PATCH] Refactor Rust lint test structure to use RuffTestFixture (#20689) Co-authored-by: Micha Reiser --- Cargo.lock | 1 + crates/ruff/Cargo.toml | 1 + crates/ruff/tests/cli/lint.rs | 6123 +++++++++++++++ crates/ruff/tests/cli/main.rs | 177 + ...ort_convention_unused_aliased_import.snap} | 0 ...on_unused_aliased_import_no_conflict.snap} | 0 .../cli__lint__output_format_azure.snap} | 0 .../cli__lint__output_format_concise.snap} | 0 .../cli__lint__output_format_full.snap} | 0 .../cli__lint__output_format_github.snap} | 0 .../cli__lint__output_format_gitlab.snap} | 0 .../cli__lint__output_format_grouped.snap} | 0 .../cli__lint__output_format_json-lines.snap} | 0 .../cli__lint__output_format_json.snap} | 0 .../cli__lint__output_format_junit.snap} | 0 .../cli__lint__output_format_pylint.snap} | 0 .../cli__lint__output_format_rdjson.snap} | 0 .../cli__lint__output_format_sarif.snap} | 3 +- ...nt__output_format_show_fixes_concise.snap} | 0 ..._lint__output_format_show_fixes_full.snap} | 0 ...nt__output_format_show_fixes_grouped.snap} | 0 ...arn_invalid_noqa_with_no_diagnostics.snap} | 0 crates/ruff/tests/lint.rs | 6597 ----------------- 23 files changed, 6304 insertions(+), 6598 deletions(-) create mode 100644 crates/ruff/tests/cli/lint.rs create mode 100644 crates/ruff/tests/cli/main.rs rename crates/ruff/tests/{snapshots/lint__flake8_import_convention_unused_aliased_import.snap => cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import.snap} (100%) rename crates/ruff/tests/{snapshots/lint__flake8_import_convention_unused_aliased_import_no_conflict.snap => cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import_no_conflict.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_azure.snap => cli/snapshots/cli__lint__output_format_azure.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_concise.snap => cli/snapshots/cli__lint__output_format_concise.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_full.snap => cli/snapshots/cli__lint__output_format_full.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_github.snap => cli/snapshots/cli__lint__output_format_github.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_gitlab.snap => cli/snapshots/cli__lint__output_format_gitlab.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_grouped.snap => cli/snapshots/cli__lint__output_format_grouped.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_json-lines.snap => cli/snapshots/cli__lint__output_format_json-lines.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_json.snap => cli/snapshots/cli__lint__output_format_json.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_junit.snap => cli/snapshots/cli__lint__output_format_junit.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_pylint.snap => cli/snapshots/cli__lint__output_format_pylint.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_rdjson.snap => cli/snapshots/cli__lint__output_format_rdjson.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_sarif.snap => cli/snapshots/cli__lint__output_format_sarif.snap} (99%) rename crates/ruff/tests/{snapshots/lint__output_format_show_fixes_concise.snap => cli/snapshots/cli__lint__output_format_show_fixes_concise.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_show_fixes_full.snap => cli/snapshots/cli__lint__output_format_show_fixes_full.snap} (100%) rename crates/ruff/tests/{snapshots/lint__output_format_show_fixes_grouped.snap => cli/snapshots/cli__lint__output_format_show_fixes_grouped.snap} (100%) rename crates/ruff/tests/{snapshots/lint__warn_invalid_noqa_with_no_diagnostics.snap => cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap} (100%) delete mode 100644 crates/ruff/tests/lint.rs diff --git a/Cargo.lock b/Cargo.lock index 80fcc63080..29debc0a8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2777,6 +2777,7 @@ dependencies = [ "ruff_python_ast", "ruff_python_formatter", "ruff_python_parser", + "ruff_python_trivia", "ruff_server", "ruff_source_file", "ruff_text_size", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index e59e7508cb..8c5728477b 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -72,6 +72,7 @@ dunce = { workspace = true } indoc = { workspace = true } insta = { workspace = true, features = ["filters", "json"] } insta-cmd = { workspace = true } +ruff_python_trivia = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs new file mode 100644 index 0000000000..4080f27cc7 --- /dev/null +++ b/crates/ruff/tests/cli/lint.rs @@ -0,0 +1,6123 @@ +//! Tests the interaction of the `lint` configuration section + +use std::fs; +use std::process::Command; +use std::str; + +use anyhow::Result; + +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; + +use crate::CliTest; + +const BIN_NAME: &str = "ruff"; +const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"]; + +impl CliTest { + fn check_command(&self) -> Command { + let mut command = self.command(); + command.args(STDIN_BASE_OPTIONS); + command + } +} + +#[test] +fn top_level_options() -> Result<()> { + let test = CliTest::new()?; + test.write_file( + "ruff.toml", + r#" +extend-select = ["B", "Q"] + +[flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(test.check_command() + .arg("--config") + .arg("ruff.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:5: Q000 [*] Double quotes found but single quotes preferred + test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading + test.py:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'extend-select' -> 'lint.extend-select' + - 'flake8-quotes' -> 'lint.flake8-quotes' + "); + + Ok(()) +} + +#[test] +fn lint_options() -> Result<()> { + let case = CliTest::with_file( + "ruff.toml", + r#" +[lint] +extend-select = ["B", "Q"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!( + case.check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Tests that configurations from the top-level and `lint` section are merged together. +#[test] +fn mixed_levels() -> Result<()> { + let test = CliTest::new()?; + test.write_file( + "ruff.toml", + r#" +extend-select = ["B", "Q"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(test.check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'extend-select' -> 'lint.extend-select' + "); + + Ok(()) +} + +/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific). +#[test] +fn precedence() -> Result<()> { + let test = CliTest::new()?; + test.write_file( + "ruff.toml", + r#" +[lint] +extend-select = ["B", "Q"] + +[flake8-quotes] +inline-quotes = "double" + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(test.check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + -:1:5: Q000 [*] Double quotes found but single quotes preferred + -:1:5: B005 Using `.strip()` with multi-character strings is misleading + -:1:19: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'flake8-quotes' -> 'lint.flake8-quotes' + "); + + Ok(()) +} + +#[test] +fn exclude() -> Result<()> { + let case = CliTest::new()?; + + case.write_file( + "ruff.toml", + r#" +extend-select = ["B", "Q"] +extend-exclude = ["out"] + +[lint] +exclude = ["test.py", "generated.py"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + case.write_file( + "main.py", + r#"from test import say_hy + +if __name__ == "__main__": + say_hy("dear Ruff contributor") +"#, + )?; + + // Excluded file but passed to the CLI directly, should be linted + case.write_file( + "test.py", + r#"def say_hy(name: str): + print(f"Hy {name}")"#, + )?; + + case.write_file( + "generated.py", + r#"NUMBERS = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 +] +OTHER = "OTHER" +"#, + )?; + + case.write_file("out/a.py", r#"a = "a""#)?; + + assert_cmd_snapshot!( + case.check_command() + .args(["--config", "ruff.toml"]) + // Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude + .arg("test.py") + // Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options + .arg("."), @r" + success: false + exit_code: 1 + ----- stdout ----- + main.py:3:16: Q000 [*] Double quotes found but single quotes preferred + main.py:4:12: Q000 [*] Double quotes found but single quotes preferred + test.py:2:15: Q000 [*] Double quotes found but single quotes preferred + Found 3 errors. + [*] 3 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'extend-select' -> 'lint.extend-select' + "); + + Ok(()) +} + +/// Regression test for +#[test] +fn deduplicate_directory_and_explicit_file() -> Result<()> { + let case = CliTest::new()?; + + case.write_file( + "ruff.toml", + r#" +[lint] +exclude = ["main.py"] +"#, + )?; + + case.write_file("main.py", "import os\n")?; + + assert_cmd_snapshot!( + case.check_command() + .args(["--config", "ruff.toml"]) + .arg(".") + // Explicitly pass main.py, should be linted regardless of it being excluded by lint.exclude + .arg("main.py"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + " + ); + + Ok(()) +} + +#[test] +fn exclude_stdin() -> Result<()> { + let case = CliTest::with_file( + "ruff.toml", + r#" +extend-select = ["B", "Q"] + +[lint] +exclude = ["generated.py"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!( + case.check_command() + .args(["--config", "ruff.toml"]) + .args(["--stdin-filename", "generated.py"]) + .arg("-") + .pass_stdin(r#" +from test import say_hy + +if __name__ == "__main__": + say_hy("dear Ruff contributor") +"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + generated.py:4:16: Q000 [*] Double quotes found but single quotes preferred + generated.py:5:12: Q000 [*] Double quotes found but single quotes preferred + Found 2 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'extend-select' -> 'lint.extend-select' + "); + + Ok(()) +} + +#[test] +fn line_too_long_width_override() -> Result<()> { + let test = CliTest::new()?; + test.write_file( + "ruff.toml", + r#" +line-length = 80 +select = ["E501"] + +[pycodestyle] +max-line-length = 100 +"#, + )?; + + assert_cmd_snapshot!(test.check_command() + .arg("--config") + .arg("ruff.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#" +# longer than 80, but less than 100 +_ = "---------------------------------------------------------------------------亜亜亜亜亜亜" +# longer than 100 +_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜" +"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:5:91: E501 Line too long (109 > 100) + Found 1 error. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'select' -> 'lint.select' + - 'pycodestyle' -> 'lint.pycodestyle' + "); + + Ok(()) +} + +#[test] +fn per_file_ignores_stdin() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +extend-select = ["B", "Q"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .args(["--stdin-filename", "generated.py"]) + .args(["--per-file-ignores", "generated.py:Q"]) + .arg("-") + .pass_stdin(r#" +import os + +from test import say_hy + +if __name__ == "__main__": + say_hy("dear Ruff contributor") +"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + generated.py:2:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'extend-select' -> 'lint.extend-select' + "); + + Ok(()) +} + +#[test] +fn extend_per_file_ignores_stdin() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +extend-select = ["B", "Q"] + +[lint.flake8-quotes] +inline-quotes = "single" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .args(["--stdin-filename", "generated.py"]) + .args(["--extend-per-file-ignores", "generated.py:Q"]) + .arg("-") + .pass_stdin(r#" +import os + +from test import say_hy + +if __name__ == "__main__": + say_hy("dear Ruff contributor") +"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + generated.py:2:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: + - 'extend-select' -> 'lint.extend-select' + "); + + Ok(()) +} + +/// Regression test for [#8858](https://github.com/astral-sh/ruff/issues/8858) +#[test] +fn parent_configuration_override() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["ALL"] +"#, + )?; + + fixture.write_file( + "subdirectory/ruff.toml", + r#" +[lint] +ignore = ["D203", "D212"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .current_dir(fixture.root().join("subdirectory")) + , @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: No Python files found under the given path(s) + "); + + Ok(()) +} + +#[test] +fn nonexistent_config_file() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "foo.toml", "."]), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'foo.toml' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + It looks like you were trying to pass a path to a configuration file. + The path `foo.toml` does not point to a configuration file + + For more information, try '--help'. + "); +} + +#[test] +fn config_override_rejected_if_invalid_toml() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "foo = bar", "."]), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'foo = bar' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + The supplied argument is not valid TOML: + + TOML parse error at line 1, column 7 + | + 1 | foo = bar + | ^^^ + string values must be quoted, expected literal string + + For more information, try '--help'. + "); +} + +#[test] +fn too_many_config_files() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file("ruff.toml", "")?; + fixture.write_file("ruff2.toml", "")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("--config") + .arg("ruff2.toml") + .arg("."), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: You cannot specify more than one configuration file on the command line. + + tip: remove either `--config=ruff.toml` or `--config=ruff2.toml`. + For more information, try `--help`. + "); + Ok(()) +} + +#[test] +fn extend_passed_via_config_argument() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "extend = 'foo.toml'", "."]), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'extend = 'foo.toml'' for '--config ' + + tip: Cannot include `extend` in a --config flag value + + For more information, try '--help'. + "); +} + +#[test] +fn nonexistent_extend_file() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +extend = "ruff2.toml" +"#, + )?; + + fixture.write_file( + "ruff2.toml", + r#" +extend = "ruff3.toml" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Failed to load extended configuration `[TMP]/ruff3.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml`) + Cause: Failed to read [TMP]/ruff3.toml + Cause: No such file or directory (os error 2) + "); + + Ok(()) +} + +#[test] +fn circular_extend() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +extend = "ruff2.toml" +"#, + )?; + fixture.write_file( + "ruff2.toml", + r#" +extend = "ruff3.toml" +"#, + )?; + fixture.write_file( + "ruff3.toml", + r#" +extend = "ruff.toml" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command(), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Circular configuration detected: `[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml` extends `[TMP]/ruff.toml` + "); + + Ok(()) +} + +#[test] +fn parse_error_extends() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +extend = "ruff2.toml" +"#, + )?; + fixture.write_file( + "ruff2.toml", + r#" +[lint] +select = [E501] +"#, + )?; + + assert_cmd_snapshot!( + fixture.check_command(), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Failed to load extended configuration `[TMP]/ruff2.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml`) + Cause: Failed to parse [TMP]/ruff2.toml + Cause: TOML parse error at line 3, column 11 + | + 3 | select = [E501] + | ^^^^ + string values must be quoted, expected literal string + "); + + Ok(()) +} + +#[test] +fn config_file_and_isolated() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file("ruff.toml", "")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("--isolated") + .arg("."), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: The argument `--config=ruff.toml` cannot be used with `--isolated` + + tip: You cannot specify a configuration file and also specify `--isolated`, + as `--isolated` causes ruff to ignore all configuration files. + For more information, try `--help`. + "); + Ok(()) +} + +#[test] +fn config_override_via_cli() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +line-length = 100 + +[lint] +select = ["I"] + +[lint.isort] +combine-as-imports = true + "#, + )?; + let test_code = r#" +from foo import ( + aaaaaaaaaaaaaaaaaaa, + bbbbbbbbbbb as bbbbbbbbbbbbbbbb, + cccccccccccccccc, + ddddddddddd as ddddddddddddd, + eeeeeeeeeeeeeee, + ffffffffffff as ffffffffffffff, + ggggggggggggg, + hhhhhhh as hhhhhhhhhhh, + iiiiiiiiiiiiii, + jjjjjjjjjjjjj as jjjjjj, +) + +x = "longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +"#; + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .args(["--config", "line-length=90"]) + .args(["--config", "lint.extend-select=['E501', 'F841']"]) + .args(["--config", "lint.isort.combine-as-imports = false"]) + .arg("-") + .pass_stdin(test_code), @r" + success: false + exit_code: 1 + ----- stdout ----- + -:2:1: I001 [*] Import block is un-sorted or un-formatted + -:15:91: E501 Line too long (97 > 90) + Found 2 errors. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + +#[test] +fn valid_toml_but_nonexistent_option_provided_via_config_argument() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args([".", "--config", "extend-select=['F481']"]), // No such code as F481! + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'extend-select=['F481']' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + Could not parse the supplied argument as a `ruff.toml` configuration option: + + Unknown rule selector: `F481` + + For more information, try '--help'. + "); +} + +#[test] +fn each_toml_option_requires_a_new_flag_1() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + // commas can't be used to delimit different config overrides; + // you need a new --config flag for each override + .args([".", "--config", "extend-select=['F841'], line-length=90"]), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'extend-select=['F841'], line-length=90' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + The supplied argument is not valid TOML: + + TOML parse error at line 1, column 23 + | + 1 | extend-select=['F841'], line-length=90 + | ^ + unexpected key or value, expected newline, `#` + + For more information, try '--help'. + "); +} + +#[test] +fn each_toml_option_requires_a_new_flag_2() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + // spaces *also* can't be used to delimit different config overrides; + // you need a new --config flag for each override + .args([".", "--config", "extend-select=['F841'] line-length=90"]), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'extend-select=['F841'] line-length=90' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + The supplied argument is not valid TOML: + + TOML parse error at line 1, column 24 + | + 1 | extend-select=['F841'] line-length=90 + | ^ + unexpected key or value, expected newline, `#` + + For more information, try '--help'. + "); +} + +#[test] +fn value_given_to_table_key_is_not_inline_table_1() { + // https://github.com/astral-sh/ruff/issues/13995 + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args([".", "--config", r#"lint.flake8-pytest-style="csv""#]), + @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'lint.flake8-pytest-style="csv"' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + `lint.flake8-pytest-style` is a table of configuration options. + Did you want to override one of the table's subkeys? + + Possible choices: + + - `lint.flake8-pytest-style.fixture-parentheses` + - `lint.flake8-pytest-style.parametrize-names-type` + - `lint.flake8-pytest-style.parametrize-values-type` + - `lint.flake8-pytest-style.parametrize-values-row-type` + - `lint.flake8-pytest-style.raises-require-match-for` + - `lint.flake8-pytest-style.raises-extend-require-match-for` + - `lint.flake8-pytest-style.mark-parentheses` + - `lint.flake8-pytest-style.warns-require-match-for` + - `lint.flake8-pytest-style.warns-extend-require-match-for` + + For more information, try '--help'. + "#); +} + +#[test] +fn value_given_to_table_key_is_not_inline_table_2() { + // https://github.com/astral-sh/ruff/issues/13995 + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args([".", "--config", r#"lint=123"#]), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'lint=123' for '--config ' + + tip: A `--config` flag must either be a path to a `.toml` configuration file + or a TOML ` = ` pair overriding a specific configuration + option + + `lint` is a table of configuration options. + Did you want to override one of the table's subkeys? + + Possible choices: + + - `lint.allowed-confusables` + - `lint.dummy-variable-rgx` + - `lint.extend-ignore` + - `lint.extend-select` + - `lint.extend-fixable` + - `lint.external` + - `lint.fixable` + - `lint.ignore` + - `lint.extend-safe-fixes` + - `lint.extend-unsafe-fixes` + - `lint.ignore-init-module-imports` + - `lint.logger-objects` + - `lint.select` + - `lint.explicit-preview-rules` + - `lint.task-tags` + - `lint.typing-modules` + - `lint.unfixable` + - `lint.per-file-ignores` + - `lint.extend-per-file-ignores` + - `lint.exclude` + - `lint.preview` + - `lint.typing-extensions` + - `lint.future-annotations` + + For more information, try '--help'. + "); +} + +#[test] +fn config_doubly_overridden_via_cli() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +line-length = 100 + +[lint] +select=["E501"] +"#, + )?; + let test_code = "x = 'longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'"; + assert_cmd_snapshot!(fixture + .check_command() + // The --line-length flag takes priority over both the config file + // and the `--config="line-length=110"` flag, + // despite them both being specified after this flag on the command line: + .args(["--line-length", "90"]) + .arg("--config") + .arg("ruff.toml") + .args(["--config", "line-length=110"]) + .arg("-") + .pass_stdin(test_code), @r" + success: false + exit_code: 1 + ----- stdout ----- + -:1:91: E501 Line too long (97 > 90) + Found 1 error. + + ----- stderr ----- + "); + Ok(()) +} + +#[test] +fn complex_config_setting_overridden_via_cli() -> Result<()> { + let fixture = CliTest::with_file("ruff.toml", "lint.select = ['N801']")?; + let test_code = "class violates_n801: pass"; + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .args(["--config", "lint.per-file-ignores = {'generated.py' = ['N801']}"]) + .args(["--stdin-filename", "generated.py"]) + .arg("-") + .pass_stdin(test_code), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + Ok(()) +} + +#[test] +fn deprecated_config_option_overridden_via_cli() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "select=['N801']", "-"]) + .pass_stdin("class lowercase: ..."), + @r" + success: false + exit_code: 1 + ----- stdout ----- + -:1:7: N801 Class name `lowercase` should use CapWords convention + Found 1 error. + + ----- stderr ----- + warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your `--config` CLI arguments: + - 'select' -> 'lint.select' + "); +} + +#[test] +fn extension() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +include = ["*.ipy"] +"#, + )?; + + fixture.write_file( + "main.ipy", + r#" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", + "metadata": {}, + "outputs": [], + "source": [ + "import os" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .args(["--extension", "ipy:ipynb"]) + .arg("."), @r" + success: false + exit_code: 1 + ----- stdout ----- + main.ipy:cell 1:1:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn warn_invalid_noqa_with_no_diagnostics() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--isolated"]) + .arg("--select") + .arg("F401") + .arg("-") + .pass_stdin( + r#" +# ruff: noqa: AAA101 +print("Hello world!") +"# + ) + ); +} + +#[test] +fn file_noqa_external() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint] +external = ["AAA"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin(r#" +# flake8: noqa: AAA101, BBB102 +import os +"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + -:3:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102 + "); + + Ok(()) +} + +#[test] +fn required_version_exact_mismatch() -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + + let fixture = CliTest::with_file( + "ruff.toml", + r#" +required-version = "0.1.0" +"#, + )?; + + insta::with_settings!({ + filters => vec![(version, "[VERSION]")] + }, { + assert_cmd_snapshot!(fixture + .check_command() + .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 fixture = CliTest::with_file( + "ruff.toml", + &format!( + r#" +required-version = "{version}" +"# + ), + )?; + + insta::with_settings!({ + filters => vec![(version, "[VERSION]")] + }, { + assert_cmd_snapshot!(fixture + .check_command() + .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 fixture = CliTest::with_file( + "ruff.toml", + &format!( + r#" +required-version = ">{version}" +"# + ), + )?; + + insta::with_settings!({ + filters => vec![(version, "[VERSION]")] + }, { + assert_cmd_snapshot!(fixture + .check_command() + .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 fixture = CliTest::with_file( + "ruff.toml", + r#" +required-version = ">=0.1.0" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .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(()) +} + +/// Expand environment variables in `--config` paths provided via the CLI. +#[test] +fn config_expand() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint] +select = ["F"] +ignore = ["F841"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("${NAME}.toml") + .env("NAME", "ruff") + .arg("-") + .pass_stdin(r#" +import os + +def func(): + x = 1 +"#), @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(()) +} + +/// Per-file selects via ! negation in per-file-ignores +#[test] +fn negated_per_file_ignores() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint.per-file-ignores] +"!selected.py" = ["RUF"] +"#, + )?; + fixture.write_file("selected.py", "")?; + fixture.write_file("ignored.py", "")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("--select") + .arg("RUF901") + , @r" + success: false + exit_code: 1 + ----- stdout ----- + selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix. + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + +#[test] +fn negated_per_file_ignores_absolute() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint.per-file-ignores] +"!src/**.py" = ["RUF"] +"#, + )?; + fixture.write_file("src/selected.py", "")?; + fixture.write_file("ignored.py", "")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("--select") + .arg("RUF901") + , @r" + success: false + exit_code: 1 + ----- stdout ----- + src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix. + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + +/// patterns are additive, can't use negative patterns to "un-ignore" +#[test] +fn negated_per_file_ignores_overlap() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint.per-file-ignores] +"*.py" = ["RUF"] +"!foo.py" = ["RUF"] +"#, + )?; + fixture.write_file("foo.py", "")?; + fixture.write_file("bar.py", "")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("--select") + .arg("RUF901") + , @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + Ok(()) +} + +#[test] +fn unused_interaction() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint] +select = ["F"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .args(["--stdin-filename", "test.py"]) + .arg("--fix") + .arg("-") + .pass_stdin(r#" +import os # F401 + +def function(): + import os # F811 + print(os.name) +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + import os # F401 + + def function(): + print(os.name) + + ----- stderr ----- + Found 1 error (1 fixed, 0 remaining). + "); + + Ok(()) +} + +#[test] +fn add_noqa() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["RUF015"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +def first_square(): + return [x * x for x in range(20)][0] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r" + def first_square(): + return [x * x for x in range(20)][0] # noqa: RUF015 + "); + + Ok(()) +} + +#[test] +fn add_noqa_multiple_codes() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["ANN001", "ANN201", "ARG001", "D103"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +def unused(x): + pass +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r" + def unused(x): # noqa: ANN001, ANN201, D103 + pass + "); + + Ok(()) +} + +#[test] +fn add_noqa_multiline_diagnostic() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["I"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +import z +import c +import a +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r" + import z # noqa: I001 + import c + import a + "); + + Ok(()) +} + +#[test] +fn add_noqa_existing_noqa() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["ANN001", "ANN201", "ARG001", "D103"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +def unused(x): # noqa: ANN001, ARG001, D103 + pass +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r" + def unused(x): # noqa: ANN001, ANN201, ARG001, D103 + pass + "); + + Ok(()) +} + +#[test] +fn add_noqa_multiline_comment() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["UP031"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +print( + """First line + second line + third line + %s""" + % name +) +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--config", "ruff.toml"]) + .arg("noqa.py") + .arg("--preview") + .args(["--add-noqa"]) + .arg("-") + .pass_stdin(r#" + +"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + let test_code = + fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file"); + + insta::assert_snapshot!(test_code, @r#" + print( + """First line + second line + third line + %s""" # noqa: UP031 + % name + ) + "#); + + Ok(()) +} + +#[test] +fn add_noqa_exclude() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +exclude = ["excluded.py"] +select = ["RUF015"] +"#, + )?; + + fixture.write_file( + "noqa.py", + r#" +def first_square(): + return [x * x for x in range(20)][0] +"#, + )?; + + fixture.write_file( + "excluded.py", + r#" +def first_square(): + return [x * x for x in range(20)][0] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--add-noqa"]), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 1 noqa directive. + "); + + Ok(()) +} + +/// Regression test for +#[test] +fn add_noqa_parent() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "noqa.py", + r#" +from foo import ( # noqa: F401 + bar +) + "#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--add-noqa") + .arg("--select=F401") + .arg("noqa.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + Ok(()) +} + +/// Infer `3.11` from `requires-python` in `pyproject.toml`. +#[test] +fn requires_python() -> Result<()> { + let fixture = CliTest::with_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" + +[tool.ruff.lint] +select = ["UP006"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("pyproject.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + let fixture2 = CliTest::with_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.8" + +[tool.ruff.lint] +select = ["UP006"] +"#, + )?; + + assert_cmd_snapshot!(fixture2 + .check_command() + .arg("--config") + .arg("pyproject.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + Ok(()) +} + +/// Infer `3.11` from `requires-python` in `pyproject.toml`. +#[test] +fn requires_python_patch() -> Result<()> { + let fixture = CliTest::with_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11.4" + +[tool.ruff.lint] +select = ["UP006"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("pyproject.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Infer `3.11` from `requires-python` in `pyproject.toml`. +#[test] +fn requires_python_equals() -> Result<()> { + let fixture = CliTest::with_file( + "pyproject.toml", + r#"[project] +requires-python = "== 3.11" + +[tool.ruff.lint] +select = ["UP006"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("pyproject.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Infer `3.11` from `requires-python` in `pyproject.toml`. +#[test] +fn requires_python_equals_patch() -> Result<()> { + let fixture = CliTest::with_file( + "pyproject.toml", + r#"[project] +requires-python = "== 3.11.4" + +[tool.ruff.lint] +select = ["UP006"] +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("pyproject.toml") + .args(["--stdin-filename", "test.py"]) + .arg("-") + .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "test.py", + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("test.py") + .arg("-") + , @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.11 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#, + ); + + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool_preview_enabled() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "test.py", + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--preview") + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("test.py") + .arg("-") + , @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.pyw", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = enabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = enabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = enabled + analyze.target_version = 3.11 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#, + ); + + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool_target_version_override() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "test.py", + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .args(["--select","UP007"]) + .args(["--target-version","py310"]) + .arg("test.py") + .arg("-") + , @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.10 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.10 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.10 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#, + ); + + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool_with_check() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "test.py", + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .args(["--select","UP007"]) + .arg(".") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:31: UP007 [*] Use `X | Y` for type annotations + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "###); + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<-- no [tool.ruff] +/// ├── ruff.toml #<-- no `target-version` +/// └── test.py +/// ``` +#[test] +fn requires_python_ruff_toml_no_target_fallback() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#"[lint] +select = ["UP007"] +"#, + )?; + + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "test.py", + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("test.py") + .arg("--show-settings"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.11 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#, + ); + + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<-- no [tool.ruff] +/// ├── ruff.toml #<-- no `target-version` +/// └── test.py +/// ``` +#[test] +fn requires_python_ruff_toml_no_target_fallback_check() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#"[lint] +select = ["UP007"] +"#, + )?; + + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "test.py", + r#" +from typing import Union;foo: Union[int, str] = 1"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("test.py") + , @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:2:31: UP007 [*] Use `X | Y` for type annotations + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + +/// ``` +/// tmp +/// ├── foo +/// │ ├── pyproject.toml #<-- no [tool.ruff], no `requires-python` +/// │ └── test.py +/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python` +/// ``` +#[test] +fn requires_python_pyproject_toml_above() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "foo/pyproject.toml", + r#"[project] +"#, + )?; + + fixture.write_file( + "foo/test.py", + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("foo/test.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.11 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + + Ok(()) +} + +/// ``` +/// tmp +/// ├── foo +/// │ ├── pyproject.toml #<-- has [tool.ruff], no `requires-python` +/// │ └── test.py +/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python` +/// ``` +#[test] +fn requires_python_pyproject_toml_above_with_tool() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "foo/pyproject.toml", + r#" +[tool.ruff] +target-version = "py310" +"#, + )?; + + fixture.write_file( + "foo/test.py", + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("foo/test.py"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + + # General Settings + cache_dir = "[TMP]/foo/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/foo" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/foo" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.10 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/foo", + "[TMP]/foo/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.10 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.10 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#); + Ok(()) +} + +/// ``` +/// tmp +/// ├── foo +/// │ ├── pyproject.toml #<-- no [tool.ruff] +/// │ └── test.py +/// └── ruff.toml #<-- no `target-version` +/// ``` +#[test] +fn requires_python_ruff_toml_above() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +[lint] +select = ["UP007"] +"#, + )?; + + fixture.write_file( + "foo/pyproject.toml", + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + fixture.write_file( + "foo/test.py", + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .arg("foo/test.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = none + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.9 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.9 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .arg("test.py") + .current_dir(fixture.root().join("foo")), @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = none + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.9 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.9 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#); + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml <-- requires >=3.10 +/// ├── ruff.toml <--- extends base +/// ├── shared +/// │ └── base_config.toml <-- targets 3.11 +/// └── test.py +/// ``` +#[test] +fn requires_python_extend_from_shared_config() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "ruff.toml", + r#" +extend = "./shared/base_config.toml" +[lint] +select = ["UP007"] +"#, + )?; + + fixture.write_file( + "pyproject.toml", + r#"[project] +requires-python = ">= 3.10" +"#, + )?; + + fixture.write_file( + "shared/base_config.toml", + r#" +target-version = "py311" +"#, + )?; + + fixture.write_file( + "test.py", + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--show-settings") + .arg("test.py"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.10 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + linter.typing_extensions = true + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + numpy.typing = npt, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pylint.max_nested_blocks = 5 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.10 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.10 + analyze.string_imports = disabled + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "#); + Ok(()) +} + +#[test] +fn checks_notebooks_in_stable() -> anyhow::Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "main.ipynb", + r#" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", + "metadata": {}, + "outputs": [], + "source": [ + "import random" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--select") + .arg("F401") + , @r" + success: false + exit_code: 1 + ----- stdout ----- + main.ipynb:cell 1:1:8: F401 [*] `random` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + +/// Verify that implicit namespace packages are detected even when they are nested. +/// +/// See: +#[test] +fn nested_implicit_namespace_package() -> Result<()> { + let fixture = CliTest::new()?; + + fixture.write_file("foo/__init__.py", "")?; + fixture.write_file("foo/bar/baz/__init__.py", "")?; + fixture.write_file("foo/bar/baz/bop.py", "")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--select") + .arg("INP") + , @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--select") + .arg("INP") + .arg("--preview") + , @r" + success: false + exit_code: 1 + ----- stdout ----- + foo/bar/baz/__init__.py:1:1: INP001 File `foo/bar/baz/__init__.py` declares a package, but is nested under an implicit namespace package. Add an `__init__.py` to `foo/bar`. + Found 1 error. + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint.flake8-import-conventions.aliases] +"module.name" = "invalid.alias" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin("") + , @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Failed to load configuration `[TMP]/ruff.toml` + Cause: Failed to parse [TMP]/ruff.toml + Cause: TOML parse error at line 3, column 17 + | + 3 | "module.name" = "invalid.alias" + | ^^^^^^^^^^^^^^^ + invalid value: string "invalid.alias", expected a Python identifier + "#); + Ok(()) +} + +#[test] +fn flake8_import_convention_invalid_aliases_config_extend_alias_name() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint.flake8-import-conventions.extend-aliases] +"module.name" = "__debug__" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin("") + , @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Failed to load configuration `[TMP]/ruff.toml` + Cause: Failed to parse [TMP]/ruff.toml + Cause: TOML parse error at line 3, column 17 + | + 3 | "module.name" = "__debug__" + | ^^^^^^^^^^^ + invalid value: string "__debug__", expected an assignable Python identifier + "#); + Ok(()) +} + +#[test] +fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint.flake8-import-conventions.aliases] +"module..invalid" = "alias" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin("") + , @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Failed to load configuration `[TMP]/ruff.toml` + Cause: Failed to parse [TMP]/ruff.toml + Cause: TOML parse error at line 3, column 1 + | + 3 | "module..invalid" = "alias" + | ^^^^^^^^^^^^^^^^^ + invalid value: string "module..invalid", expected a sequence of Python identifiers delimited by periods + "#); + Ok(()) +} + +#[test] +fn flake8_import_convention_nfkc_normalization() -> Result<()> { + let fixture = CliTest::with_file( + "ruff.toml", + r#" +[lint.flake8-import-conventions.aliases] +"test.module" = "_﹏𝘥𝘦𝘣𝘶𝘨﹏﹏" +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--config") + .arg("ruff.toml") + .arg("-") + .pass_stdin("") + , @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Invalid alias for module 'test.module': alias normalizes to '__debug__', which is not allowed. + "###); + Ok(()) +} + +#[test] +fn flake8_import_convention_unused_aliased_import() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["import pandas"]"#) + .args(["--select", "I002,ICN001,F401"]) + .args(["--stdin-filename", "test.py"]) + .arg("--unsafe-fixes") + .arg("--fix") + .arg("-") + .pass_stdin("1") + ); +} + +#[test] +fn flake8_import_convention_unused_aliased_import_no_conflict() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#) + .args(["--select", "I002,ICN001,F401"]) + .args(["--stdin-filename", "test.py"]) + .arg("--unsafe-fixes") + .arg("--fix") + .arg("-") + .pass_stdin("1") + ); +} + +// https://github.com/astral-sh/ruff/issues/19842 +#[test] +fn pyupgrade_up026_respects_isort_required_import_fix() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .arg("--isolated") + .arg("check") + .arg("-") + .args(["--select", "I002,UP026"]) + .arg("--config") + .arg(r#"lint.isort.required-imports=["import mock"]"#) + .arg("--fix") + .arg("--no-cache") + .pass_stdin("1\n"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + import mock + 1 + + ----- stderr ----- + Found 1 error (1 fixed, 0 remaining). + " + ); +} + +// https://github.com/astral-sh/ruff/issues/19842 +#[test] +fn pyupgrade_up026_respects_isort_required_import_from_fix() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .arg("--isolated") + .arg("check") + .arg("-") + .args(["--select", "I002,UP026"]) + .arg("--config") + .arg(r#"lint.isort.required-imports = ["from mock import mock"]"#) + .arg("--fix") + .arg("--no-cache") + .pass_stdin("from mock import mock\n"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + from mock import mock + + ----- stderr ----- + All checks passed! + " + ); +} + +// See: https://github.com/astral-sh/ruff/issues/16177 +#[test] +fn flake8_pyi_redundant_none_literal() { + let snippet = r#" +from typing import Literal + +# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements +# but not both, as if both were autofixed it would result in `None | None`, +# which leads to a `TypeError` at runtime. +a: Literal[None,] | Literal[None,] +b: Literal[None] | Literal[None] +c: Literal[None] | Literal[None,] +d: Literal[None,] | Literal[None] +"#; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--select", "PYI061"]) + .args(["--stdin-filename", "test.py"]) + .arg("--preview") + .arg("--diff") + .arg("-") + .pass_stdin(snippet), @r" + success: false + exit_code: 1 + ----- stdout ----- + --- test.py + +++ test.py + @@ -4,7 +4,7 @@ + # For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements + # but not both, as if both were autofixed it would result in `None | None`, + # which leads to a `TypeError` at runtime. + -a: Literal[None,] | Literal[None,] + -b: Literal[None] | Literal[None] + -c: Literal[None] | Literal[None,] + -d: Literal[None,] | Literal[None] + +a: None | Literal[None,] + +b: None | Literal[None] + +c: None | Literal[None,] + +d: None | Literal[None] + + + ----- stderr ----- + Would fix 4 errors. + "); +} + +/// Test that private, old-style `TypeVar` generics +/// 1. Get replaced with PEP 695 type parameters (UP046, UP047) +/// 2. Get renamed to remove leading underscores (UP049) +/// 3. Emit a warning that the standalone type variable is now unused (PYI018) +/// 4. Remove the now-unused `Generic` import +#[test] +fn pep695_generic_rename() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--select", "F401,PYI018,UP046,UP047,UP049"]) + .args(["--stdin-filename", "test.py"]) + .arg("--unsafe-fixes") + .arg("--fix") + .arg("--preview") + .arg("--target-version=py312") + .arg("-") + .pass_stdin( + r#" +from typing import Generic, TypeVar +_T = TypeVar("_T") + +class OldStyle(Generic[_T]): + var: _T + +def func(t: _T) -> _T: + x: _T + return x +"# + ), + @r" + success: true + exit_code: 0 + ----- stdout ----- + + + class OldStyle[T]: + var: T + + def func[T](t: T) -> T: + x: T + return x + + ----- stderr ----- + Found 7 errors (7 fixed, 0 remaining). + " + ); +} + +/// Test that we do not rename two different type parameters to the same name +/// in one execution of Ruff (autofixing this to `class Foo[T, T]: ...` would +/// introduce invalid syntax) +#[test] +fn type_parameter_rename_isolation() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--select", "UP049"]) + .args(["--stdin-filename", "test.py"]) + .arg("--unsafe-fixes") + .arg("--fix") + .arg("--preview") + .arg("--target-version=py312") + .arg("-") + .pass_stdin( + r#" +class Foo[_T, __T]: + pass +"# + ), + @r" + success: false + exit_code: 1 + ----- stdout ----- + + class Foo[T, __T]: + pass + + ----- stderr ----- + test.py:2:14: UP049 Generic class uses private type parameters + Found 2 errors (1 fixed, 1 remaining). + " + ); +} + +/// construct a directory tree with this structure: +/// . +/// ├── abc +/// │ └── __init__.py +/// ├── collections +/// │ ├── __init__.py +/// │ ├── abc +/// │ │ └── __init__.py +/// │ └── foobar +/// │ └── __init__.py +/// ├── foobar +/// │ ├── __init__.py +/// │ ├── abc +/// │ │ └── __init__.py +/// │ └── collections +/// │ ├── __init__.py +/// │ ├── abc +/// │ │ └── __init__.py +/// │ └── foobar +/// │ └── __init__.py +/// ├── ruff.toml +/// └── urlparse +/// └── __init__.py +fn create_a005_module_structure(fixture: &CliTest) -> Result<()> { + // Create module structure + fixture.write_file("abc/__init__.py", "")?; + fixture.write_file("collections/__init__.py", "")?; + fixture.write_file("collections/abc/__init__.py", "")?; + fixture.write_file("collections/foobar/__init__.py", "")?; + fixture.write_file("foobar/__init__.py", "")?; + fixture.write_file("foobar/abc/__init__.py", "")?; + fixture.write_file("foobar/collections/__init__.py", "")?; + fixture.write_file("foobar/collections/abc/__init__.py", "")?; + fixture.write_file("urlparse/__init__.py", "")?; + // also create a ruff.toml to mark the project root + fixture.write_file("ruff.toml", "")?; + + Ok(()) +} + +/// Test A005 with `strict-checking = true` +#[test] +fn a005_module_shadowing_strict() -> Result<()> { + let fixture = CliTest::new()?; + create_a005_module_structure(&fixture)?; + + assert_cmd_snapshot!(fixture.check_command() + .arg("--config") + .arg(r#"lint.flake8-builtins.strict-checking = true"#) + .args(["--select", "A005"]), + @r" + success: false + exit_code: 1 + ----- stdout ----- + abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module + collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module + collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module + foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module + foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module + foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module + Found 6 errors. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Test A005 with `strict-checking = false` +#[test] +fn a005_module_shadowing_non_strict() -> Result<()> { + let fixture = CliTest::new()?; + create_a005_module_structure(&fixture)?; + + assert_cmd_snapshot!(fixture.check_command() + .arg("--config") + .arg(r#"lint.flake8-builtins.strict-checking = false"#) + .args(["--select", "A005"]), + @r" + success: false + exit_code: 1 + ----- stdout ----- + abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module + collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module + Found 2 errors. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Test A005 with `strict-checking` unset +/// +/// This should match the non-strict version directly above +/// Test A005 with `strict-checking` default (should be `false`) +#[test] +fn a005_module_shadowing_strict_default() -> Result<()> { + let fixture = CliTest::new()?; + create_a005_module_structure(&fixture)?; + + assert_cmd_snapshot!(fixture.check_command() + .args(["--select", "A005"]), + @r" + success: false + exit_code: 1 + ----- stdout ----- + abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module + collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module + Found 2 errors. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Test that the linter respects per-file-target-version. +#[test] +fn per_file_target_version_linter() { + // without per-file-target-version, there should be one UP046 error + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--target-version", "py312"]) + .args(["--select", "UP046"]) // only triggers on 3.12+ + .args(["--stdin-filename", "test.py"]) + .arg("--preview") + .arg("-") + .pass_stdin(r#" +from typing import Generic, TypeVar + +T = TypeVar("T") + +class A(Generic[T]): + var: T +"#), + @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters + Found 1 error. + No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). + + ----- stderr ----- + " + ); + + // with per-file-target-version, there should be no errors because the new generic syntax is + // unavailable + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--target-version", "py312"]) + .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) + .args(["--select", "UP046"]) // only triggers on 3.12+ + .args(["--stdin-filename", "test.py"]) + .arg("--preview") + .arg("-") + .pass_stdin(r#" +from typing import Generic, TypeVar + +T = TypeVar("T") + +class A(Generic[T]): + var: T +"#), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); +} + +#[test] +fn walrus_before_py38() { + // ok + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--stdin-filename", "test.py"]) + .arg("--target-version=py38") + .arg("-") + .pass_stdin(r#"(x := 1)"#), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); + + // not ok on 3.7 with preview + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--stdin-filename", "test.py"]) + .arg("--target-version=py37") + .arg("--preview") + .arg("-") + .pass_stdin(r#"(x := 1)"#), + @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:2: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) + Found 1 error. + + ----- stderr ----- + " + ); +} + +#[test] +fn match_before_py310() { + // ok on 3.10 + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--stdin-filename", "test.py"]) + .arg("--target-version=py310") + .arg("-") + .pass_stdin( + r#" +match 2: + case 1: + print("it's one") +"# + ), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); + + // ok on 3.9 without preview + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--stdin-filename", "test.py"]) + .arg("--target-version=py39") + .arg("-") + .pass_stdin( + r#" +match 2: + case 1: + print("it's one") +"# + ), + @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + Found 1 error. + + ----- stderr ----- + " + ); + + // syntax error on 3.9 with preview + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--stdin-filename", "test.py"]) + .arg("--target-version=py39") + .arg("--preview") + .arg("-") + .pass_stdin( + r#" +match 2: + case 1: + print("it's one") +"# + ), + @r" + success: false + exit_code: 1 + ----- stdout ----- + test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + Found 1 error. + + ----- stderr ----- + " + ); +} + +/// Regression test for +#[test] +fn cache_syntax_errors() -> Result<()> { + let fixture = CliTest::with_file("main.py", "match 2:\n case 1: ...")?; + + let mut cmd = fixture.command(); + // inline STDIN_BASE_OPTIONS to remove --no-cache + cmd.args(["check", "--output-format", "concise"]) + .arg("--target-version=py39") + .arg("--preview") + .arg("--quiet"); // suppress `debug build without --no-cache` warnings + + assert_cmd_snapshot!( + cmd, + @r" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + + ----- stderr ----- + " + ); + + // this should *not* be cached, like normal parse errors + assert_cmd_snapshot!( + cmd, + @r" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + + ----- stderr ----- + " + ); + + Ok(()) +} + +/// Regression test for with very helpful +/// reproduction repo here: +#[test] +fn cookiecutter_globbing() -> Result<()> { + // This is a simplified directory structure from the repo linked above. The essence of the + // problem is this `{{cookiecutter.repo_name}}` directory containing a config file with a glob. + // The absolute path of the glob contains the glob metacharacters `{{` and `}}` even though the + // user's glob does not. + let fixture = CliTest::new()?; + + fixture.write_file( + "{{cookiecutter.repo_name}}/pyproject.toml", + r#"tool.ruff.lint.per-file-ignores = { "tests/*" = ["F811"] }"#, + )?; + + // F811 example from the docs to ensure the glob still works + fixture.write_file( + "{{cookiecutter.repo_name}}/tests/maintest.py", + "import foo\nimport bar\nimport foo", + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--select=F811"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + // after removing the config file with the ignore, F811 applies, so the glob worked above + fs::remove_file( + fixture + .root() + .join("{{cookiecutter.repo_name}}/pyproject.toml"), + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--select=F811"), @r" + success: false + exit_code: 1 + ----- stdout ----- + {{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "); + + Ok(()) +} + +/// Like the test above but exercises the non-absolute path case in `PerFile::new` +#[test] +fn cookiecutter_globbing_no_project_root() -> Result<()> { + let fixture = CliTest::new()?; + + // Create the nested directory structure + fs::create_dir(fixture.root().join("{{cookiecutter.repo_name}}"))?; + + assert_cmd_snapshot!(fixture + .check_command() + .current_dir(fixture.root().join("{{cookiecutter.repo_name}}")) + .args(["--extend-per-file-ignores", "generated.py:Q"]), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: No Python files found under the given path(s) + "); + + Ok(()) +} + +/// Test that semantic syntax errors (1) are emitted, (2) are not cached, (3) don't affect the +/// reporting of normal diagnostics, and (4) are not suppressed by `select = []` (or otherwise +/// disabling all AST-based rules). +#[test] +fn semantic_syntax_errors() -> Result<()> { + let fixture = CliTest::with_file("main.py", "[(x := 1) for x in foo]")?; + let contents = "[(x := 1) for x in foo]"; + + let mut cmd = fixture.command(); + // inline STDIN_BASE_OPTIONS to remove --no-cache + cmd.args(["check", "--output-format", "concise"]) + .arg("--preview") + .arg("--quiet"); // suppress `debug build without --no-cache` warnings + + assert_cmd_snapshot!( + cmd, + @r" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable + main.py:1:20: F821 Undefined name `foo` + + ----- stderr ----- + " + ); + + // this should *not* be cached, like normal parse errors + assert_cmd_snapshot!( + cmd, + @r" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable + main.py:1:20: F821 Undefined name `foo` + + ----- stderr ----- + " + ); + + // ensure semantic errors are caught even without AST-based rules selected + assert_cmd_snapshot!( + fixture.check_command() + .args(["--config", "lint.select = []"]) + .arg("--preview") + .arg("-") + .pass_stdin(contents), + @r" + success: false + exit_code: 1 + ----- stdout ----- + -:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable + Found 1 error. + + ----- stderr ----- + " + ); + + Ok(()) +} + +/// Regression test for . +/// +/// `lint.typing-extensions = false` with Python 3.9 should disable the PYI019 lint because it would +/// try to import `Self` from `typing_extensions` +#[test] +fn combine_typing_extensions_config() { + let contents = " +from typing import TypeVar +T = TypeVar('T') +class Foo: + def f(self: T) -> T: ... +"; + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--config", "lint.typing-extensions = false"]) + .arg("--select=PYI019") + .arg("--target-version=py39") + .arg("-") + .pass_stdin(contents), + @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + " + ); +} + +#[test_case::test_case("concise")] +#[test_case::test_case("full")] +#[test_case::test_case("json")] +#[test_case::test_case("json-lines")] +#[test_case::test_case("junit")] +#[test_case::test_case("grouped")] +#[test_case::test_case("github")] +#[test_case::test_case("gitlab")] +#[test_case::test_case("pylint")] +#[test_case::test_case("rdjson")] +#[test_case::test_case("azure")] +#[test_case::test_case("sarif")] +fn output_format(output_format: &str) -> Result<()> { + const CONTENT: &str = "\ +import os # F401 +x = y # F821 +match 42: # invalid-syntax + case _: ... +"; + + let fixture = CliTest::with_settings(|_project_dir, mut settings| { + // JSON double escapes backslashes + settings.add_filter(r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#); + + settings + })?; + + fixture.write_file("input.py", CONTENT)?; + + let snapshot = format!("output_format_{output_format}"); + + assert_cmd_snapshot!( + snapshot, + fixture.command().args([ + "check", + "--no-cache", + "--output-format", + output_format, + "--select", + "F401,F821", + "--target-version", + "py39", + "input.py", + ]) + ); + + Ok(()) +} + +#[test_case::test_case("concise"; "concise_show_fixes")] +#[test_case::test_case("full"; "full_show_fixes")] +#[test_case::test_case("grouped"; "grouped_show_fixes")] +fn output_format_show_fixes(output_format: &str) -> Result<()> { + let fixture = CliTest::with_file("input.py", "import os # F401")?; + let snapshot = format!("output_format_show_fixes_{output_format}"); + + assert_cmd_snapshot!( + snapshot, + fixture.command().args([ + "check", + "--no-cache", + "--output-format", + output_format, + "--select", + "F401", + "--fix", + "--show-fixes", + "input.py", + ]) + ); + + Ok(()) +} + +#[test] +fn up045_nested_optional_flatten_all() { + let contents = "\ +from typing import Optional +nested_optional: Optional[Optional[Optional[str]]] = None +"; + + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--select", "UP045", "--diff", "--target-version", "py312"]) + .arg("-") + .pass_stdin(contents), + @r" + success: false + exit_code: 1 + ----- stdout ----- + @@ -1,2 +1,2 @@ + from typing import Optional + -nested_optional: Optional[Optional[Optional[str]]] = None + +nested_optional: str | None = None + + + ----- stderr ----- + Would fix 1 error. + ", + ); +} + +#[test] +fn show_fixes_in_full_output_with_preview_enabled() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--no-cache", "--output-format", "full"]) + .args(["--select", "F401"]) + .arg("--preview") + .arg("-") + .pass_stdin("import math"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + F401 [*] `math` imported but unused + --> -:1:8 + | + 1 | import math + | ^^^^ + | + help: Remove unused import: `math` + - import math + + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + ", + ); +} + +#[test] +fn rule_panic_mixed_results_concise() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file("normal.py", "import os")?; + fixture.write_file("panic.py", "print('hello, world!')")?; + + assert_cmd_snapshot!( + fixture.check_command() + .args(["--select", "RUF9", "--preview"]) + .args(["normal.py", "panic.py"]), + @r" + success: false + exit_code: 2 + ----- stdout ----- + normal.py:1:1: RUF900 Hey this is a stable test rule. + normal.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix. + normal.py:1:1: RUF902 Hey this is a stable test rule with an unsafe fix. + normal.py:1:1: RUF903 Hey this is a stable test rule with a display only fix. + normal.py:1:1: RUF911 Hey this is a preview test rule. + normal.py:1:1: RUF950 Hey this is a test rule that was redirected from another. + panic.py: panic: Panicked at when checking `[TMP]/panic.py`: `This is a fake panic for testing.` + Found 7 errors. + [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option). + + ----- stderr ----- + error: Panic during linting indicates a bug in Ruff. If you could open an issue at: + + https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D + + ...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative! + "); + + Ok(()) +} + +#[test] +fn rule_panic_mixed_results_full() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file("normal.py", "import os")?; + fixture.write_file("panic.py", "print('hello, world!')")?; + + assert_cmd_snapshot!( + fixture.command() + .args(["check", "--select", "RUF9", "--preview", "--output-format=full", "--no-cache"]) + .args(["normal.py", "panic.py"]), + @r" + success: false + exit_code: 2 + ----- stdout ----- + RUF900 Hey this is a stable test rule. + --> normal.py:1:1 + + RUF901 [*] Hey this is a stable test rule with a safe fix. + --> normal.py:1:1 + 1 + # fix from stable-test-rule-safe-fix + 2 | import os + + RUF902 Hey this is a stable test rule with an unsafe fix. + --> normal.py:1:1 + + RUF903 Hey this is a stable test rule with a display only fix. + --> normal.py:1:1 + + RUF911 Hey this is a preview test rule. + --> normal.py:1:1 + + RUF950 Hey this is a test rule that was redirected from another. + --> normal.py:1:1 + + panic: Panicked at when checking `[TMP]/panic.py`: `This is a fake panic for testing.` + --> panic.py:1:1 + info: This indicates a bug in Ruff. + info: If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bpanic%5D, we'd be very appreciative! + info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information + + Found 7 errors. + [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option). + + ----- stderr ----- + error: Panic during linting indicates a bug in Ruff. If you could open an issue at: + + https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D + + ...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative! + "); + + Ok(()) +} + +/// Test that the same rule fires across all supported extensions, but not on unsupported files +#[test] +fn supported_file_extensions() -> Result<()> { + let fixture = CliTest::new()?; + + // Create files of various types + // text file + fixture.write_file("src/thing.txt", "hello world\n")?; + // regular python + fixture.write_file("src/thing.py", "import os\nprint('hello world')\n")?; + // python typestub + fixture.write_file("src/thing.pyi", "import os\nclass foo:\n ...\n")?; + // windows gui + fixture.write_file("src/thing.pyw", "import os\nprint('hello world')\n")?; + // cython + fixture.write_file( + "src/thing.pyx", + "import os\ncdef int add(int a, int b):\n return a + b\n", + )?; + // notebook + fixture.write_file( + "src/thing.ipynb", + r#" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", + "metadata": {}, + "outputs": [], + "source": [ + "import os" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} +"#, + )?; + + assert_cmd_snapshot!( + fixture.check_command() + .args(["--select", "F401"]) + .arg("src"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + src/thing.ipynb:cell 1:1:8: F401 [*] `os` imported but unused + src/thing.py:1:8: F401 [*] `os` imported but unused + src/thing.pyi:1:8: F401 [*] `os` imported but unused + Found 3 errors. + [*] 3 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} + +/// Test that the same rule fires across all supported extensions, but not on unsupported files +#[test] +fn supported_file_extensions_preview_enabled() -> Result<()> { + let fixture = CliTest::new()?; + + // Create files of various types + // text file + fixture.write_file("src/thing.txt", "hello world\n")?; + // regular python + fixture.write_file("src/thing.py", "import os\nprint('hello world')\n")?; + // python typestub + fixture.write_file("src/thing.pyi", "import os\nclass foo:\n ...\n")?; + // windows gui + fixture.write_file("src/thing.pyw", "import os\nprint('hello world')\n")?; + // cython + fixture.write_file( + "src/thing.pyx", + "import os\ncdef int add(int a, int b):\n return a + b\n", + )?; + // notebook + fixture.write_file( + "src/thing.ipynb", + r#" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", + "metadata": {}, + "outputs": [], + "source": [ + "import os" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} +"#, + )?; + + assert_cmd_snapshot!( + fixture.check_command() + .args(["--select", "F401", "--preview"]) + .arg("src"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + src/thing.ipynb:cell 1:1:8: F401 [*] `os` imported but unused + src/thing.py:1:8: F401 [*] `os` imported but unused + src/thing.pyi:1:8: F401 [*] `os` imported but unused + src/thing.pyw:1:8: F401 [*] `os` imported but unused + Found 4 errors. + [*] 4 fixable with the `--fix` option. + + ----- stderr ----- + "); + Ok(()) +} diff --git a/crates/ruff/tests/cli/main.rs b/crates/ruff/tests/cli/main.rs new file mode 100644 index 0000000000..91f3704e4c --- /dev/null +++ b/crates/ruff/tests/cli/main.rs @@ -0,0 +1,177 @@ +//! Test fixture utilities for ruff CLI tests +//! +//! The core concept is borrowed from ty/tests/cli/main.rs and can be extended +//! with more functionality from there in the future if needed. + +#![cfg(not(target_family = "wasm"))] + +use anyhow::{Context as _, Result}; +use insta::internals::SettingsBindDropGuard; +use insta_cmd::get_cargo_bin; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; +use tempfile::TempDir; + +mod lint; + +const BIN_NAME: &str = "ruff"; + +/// Creates a regex filter for replacing temporary directory paths in snapshots +pub(crate) fn tempdir_filter(path: impl AsRef) -> String { + format!(r"{}[\\/]?", regex::escape(path.as_ref())) +} + +/// A test fixture for running ruff CLI tests with temporary directories and files. +/// +/// This fixture provides: +/// - Temporary directory management +/// - File creation utilities +/// - Proper snapshot filtering for cross-platform compatibility +/// - Pre-configured ruff command creation +/// +/// # Example +/// +/// ```rust,no_run +/// use crate::common::RuffTestFixture; +/// +/// let fixture = RuffTestFixture::with_file("ruff.toml", "select = ['E']")?; +/// let output = fixture.command().args(["check", "."]).output()?; +/// ``` +pub(crate) struct CliTest { + _temp_dir: TempDir, + _settings_scope: SettingsBindDropGuard, + project_dir: PathBuf, +} + +impl CliTest { + /// Creates a new test fixture with an empty temporary directory. + /// + /// This sets up: + /// - A temporary directory that's automatically cleaned up + /// - Insta snapshot filters for cross-platform path compatibility + /// - Environment isolation for consistent test behavior + pub(crate) fn new() -> Result { + Self::with_settings(|_, settings| settings) + } + + pub(crate) fn with_settings( + setup_settings: impl FnOnce(&Path, insta::Settings) -> insta::Settings, + ) -> Result { + let temp_dir = TempDir::new()?; + + // Canonicalize the tempdir path because macOS uses symlinks for tempdirs + // and that doesn't play well with our snapshot filtering. + // Simplify with dunce because otherwise we get UNC paths on Windows. + let project_dir = dunce::simplified( + &temp_dir + .path() + .canonicalize() + .context("Failed to canonicalize project path")?, + ) + .to_path_buf(); + + let mut settings = setup_settings(&project_dir, insta::Settings::clone_current()); + + settings.add_filter(&tempdir_filter(project_dir.to_str().unwrap()), "[TMP]/"); + settings.add_filter(r#"\\([\w&&[^nr"]]\w|\s|\.)"#, "/$1"); + settings.add_filter(r"(Panicked at) [^:]+:\d+:\d+", "$1 "); + settings.add_filter(ruff_linter::VERSION, "[VERSION]"); + settings.add_filter( + r#"The system cannot find the file specified."#, + "No such file or directory", + ); + + let settings_scope = settings.bind_to_scope(); + + Ok(Self { + project_dir, + _temp_dir: temp_dir, + _settings_scope: settings_scope, + }) + } + + /// Creates a test fixture with a single file. + /// + /// # Arguments + /// + /// * `path` - The relative path for the file + /// * `content` - The content to write to the file + /// + /// # Example + /// + /// ```rust,no_run + /// let fixture = RuffTestFixture::with_file("ruff.toml", "select = ['E']")?; + /// ``` + pub(crate) fn with_file(path: impl AsRef, content: &str) -> Result { + let fixture = Self::new()?; + fixture.write_file(path, content)?; + Ok(fixture) + } + + /// Ensures that the parent directory of a path exists. + fn ensure_parent_directory(path: &Path) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory `{}`", parent.display()))?; + } + Ok(()) + } + + /// Writes a file to the test directory. + /// + /// Parent directories are created automatically if they don't exist. + /// Content is dedented to remove common leading whitespace for cleaner test code. + /// + /// # Arguments + /// + /// * `path` - The relative path for the file + /// * `content` - The content to write to the file + pub(crate) fn write_file(&self, path: impl AsRef, content: &str) -> Result<()> { + let path = path.as_ref(); + let file_path = self.project_dir.join(path); + + Self::ensure_parent_directory(&file_path)?; + + let content = ruff_python_trivia::textwrap::dedent(content); + fs::write(&file_path, content.as_ref()) + .with_context(|| format!("Failed to write file `{}`", file_path.display()))?; + + Ok(()) + } + + /// Returns the path to the test directory root. + pub(crate) fn root(&self) -> &Path { + &self.project_dir + } + + /// Creates a pre-configured ruff command for testing. + /// + /// The command is set up with: + /// - The correct ruff binary path + /// - Working directory set to the test directory + /// - Clean environment variables for consistent behavior + /// + /// You can chain additional arguments and options as needed. + /// + /// # Example + /// + /// ```rust,no_run + /// let output = fixture + /// .command() + /// .args(["check", "--select", "E"]) + /// .arg(".") + /// .output()?; + /// ``` + pub(crate) fn command(&self) -> Command { + let mut command = Command::new(get_cargo_bin(BIN_NAME)); + command.current_dir(&self.project_dir); + + // Unset all environment variables because they can affect test behavior. + command.env_clear(); + + command + } +} diff --git a/crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import.snap b/crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import.snap diff --git a/crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import_no_conflict.snap b/crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import_no_conflict.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__flake8_import_convention_unused_aliased_import_no_conflict.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__flake8_import_convention_unused_aliased_import_no_conflict.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_azure.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_azure.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_azure.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_azure.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_concise.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_concise.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_concise.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_concise.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_full.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_full.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_github.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_github.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_github.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_gitlab.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_gitlab.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_gitlab.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_gitlab.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_grouped.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_grouped.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_grouped.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_grouped.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_json-lines.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json-lines.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_json-lines.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_json-lines.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_json.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_json.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_json.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_json.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_junit.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_junit.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_junit.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_pylint.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_pylint.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_pylint.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_pylint.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_rdjson.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_rdjson.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_rdjson.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_rdjson.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_sarif.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_sarif.snap similarity index 99% rename from crates/ruff/tests/snapshots/lint__output_format_sarif.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_sarif.snap index 1744357e80..2d60b432fb 100644 --- a/crates/ruff/tests/snapshots/lint__output_format_sarif.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_sarif.snap @@ -1,5 +1,5 @@ --- -source: crates/ruff/tests/lint.rs +source: crates/ruff/tests/cli/lint.rs info: program: ruff args: @@ -12,6 +12,7 @@ info: - "--target-version" - py39 - input.py +snapshot_kind: text --- success: false exit_code: 1 diff --git a/crates/ruff/tests/snapshots/lint__output_format_show_fixes_concise.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_concise.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_show_fixes_concise.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_concise.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_show_fixes_full.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_full.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_show_fixes_full.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_full.snap diff --git a/crates/ruff/tests/snapshots/lint__output_format_show_fixes_grouped.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_grouped.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__output_format_show_fixes_grouped.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__output_format_show_fixes_grouped.snap diff --git a/crates/ruff/tests/snapshots/lint__warn_invalid_noqa_with_no_diagnostics.snap b/crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap similarity index 100% rename from crates/ruff/tests/snapshots/lint__warn_invalid_noqa_with_no_diagnostics.snap rename to crates/ruff/tests/cli/snapshots/cli__lint__warn_invalid_noqa_with_no_diagnostics.snap diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs deleted file mode 100644 index 1c19a40c24..0000000000 --- a/crates/ruff/tests/lint.rs +++ /dev/null @@ -1,6597 +0,0 @@ -//! Tests the interaction of the `lint` configuration section - -#![cfg(not(target_family = "wasm"))] - -use regex::escape; -use std::process::Command; -use std::str; -use std::{fs, path::Path}; - -use anyhow::Result; -use assert_fs::fixture::{ChildPath, FileTouch, PathChild}; -use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; -use tempfile::TempDir; - -const BIN_NAME: &str = "ruff"; -const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"]; - -fn tempdir_filter(path: impl AsRef) -> String { - format!(r"{}\\?/?", escape(path.as_ref().to_str().unwrap())) -} - -#[test] -fn top_level_options() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend-select = ["B", "Q"] - -[flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:5: Q000 [*] Double quotes found but single quotes preferred - test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading - test.py:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. - [*] 2 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`: - - 'extend-select' -> 'lint.extend-select' - - 'flake8-quotes' -> 'lint.flake8-quotes' - "); - }); - - Ok(()) -} - -#[test] -fn lint_options() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -extend-select = ["B", "Q"] - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("-") - .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - -:1:5: Q000 [*] Double quotes found but single quotes preferred - -:1:5: B005 Using `.strip()` with multi-character strings is misleading - -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. - [*] 2 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Tests that configurations from the top-level and `lint` section are merged together. -#[test] -fn mixed_levels() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend-select = ["B", "Q"] - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("-") - .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - -:1:5: Q000 [*] Double quotes found but single quotes preferred - -:1:5: B005 Using `.strip()` with multi-character strings is misleading - -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. - [*] 2 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`: - - 'extend-select' -> 'lint.extend-select' - "); - }); - - Ok(()) -} - -/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific). -#[test] -fn precedence() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -extend-select = ["B", "Q"] - -[flake8-quotes] -inline-quotes = "double" - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("-") - .pass_stdin(r#"a = "abcba".strip("aba")"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - -:1:5: Q000 [*] Double quotes found but single quotes preferred - -:1:5: B005 Using `.strip()` with multi-character strings is misleading - -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. - [*] 2 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`: - - 'flake8-quotes' -> 'lint.flake8-quotes' - "); - }); - - Ok(()) -} - -#[test] -fn exclude() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend-select = ["B", "Q"] -extend-exclude = ["out"] - -[lint] -exclude = ["test.py", "generated.py"] - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - fs::write( - tempdir.path().join("main.py"), - r#" -from test import say_hy - -if __name__ == "__main__": - say_hy("dear Ruff contributor") -"#, - )?; - - // Excluded file but passed to the CLI directly, should be linted - let test_path = tempdir.path().join("test.py"); - fs::write( - &test_path, - r#" -def say_hy(name: str): - print(f"Hy {name}")"#, - )?; - - fs::write( - tempdir.path().join("generated.py"), - r#"NUMBERS = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 -] -OTHER = "OTHER" -"#, - )?; - - let out_dir = tempdir.path().join("out"); - fs::create_dir(&out_dir)?; - - fs::write(out_dir.join("a.py"), r#"a = "a""#)?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - // Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude - .arg(test_path.file_name().unwrap()) - // Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options - .arg("."), @r" - success: false - exit_code: 1 - ----- stdout ----- - main.py:4:16: Q000 [*] Double quotes found but single quotes preferred - main.py:5:12: Q000 [*] Double quotes found but single quotes preferred - test.py:3:15: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. - [*] 3 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: - - 'extend-select' -> 'lint.extend-select' - "); - }); - - Ok(()) -} - -/// Regression test for -#[test] -fn deduplicate_directory_and_explicit_file() -> Result<()> { - let tempdir = TempDir::new()?; - let root = tempdir.path(); - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -exclude = ["main.py"] -"#, - )?; - - let main = root.join("main.py"); - fs::write(&main, "import os\n")?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(root) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .arg(".") - // Explicitly pass main.py, should be linted regardless of it being excluded by lint.exclude - .arg("main.py"), - @r" - success: false - exit_code: 1 - ----- stdout ----- - main.py:1:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - " - ); - }); - - Ok(()) -} - -#[test] -fn exclude_stdin() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend-select = ["B", "Q"] - -[lint] -exclude = ["generated.py"] - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .args(["--stdin-filename", "generated.py"]) - .arg("-") - .pass_stdin(r#" -from test import say_hy - -if __name__ == "__main__": - say_hy("dear Ruff contributor") -"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - generated.py:4:16: Q000 [*] Double quotes found but single quotes preferred - generated.py:5:12: Q000 [*] Double quotes found but single quotes preferred - Found 2 errors. - [*] 2 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: - - 'extend-select' -> 'lint.extend-select' - "); - }); - - Ok(()) -} - -#[test] -fn line_too_long_width_override() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -line-length = 80 -select = ["E501"] - -[pycodestyle] -max-line-length = 100 -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#" -# longer than 80, but less than 100 -_ = "---------------------------------------------------------------------------亜亜亜亜亜亜" -# longer than 100 -_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜" -"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:5:91: E501 Line too long (109 > 100) - Found 1 error. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `[TMP]/ruff.toml`: - - 'select' -> 'lint.select' - - 'pycodestyle' -> 'lint.pycodestyle' - "); - }); - - Ok(()) -} - -#[test] -fn per_file_ignores_stdin() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend-select = ["B", "Q"] - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .args(["--stdin-filename", "generated.py"]) - .args(["--per-file-ignores", "generated.py:Q"]) - .arg("-") - .pass_stdin(r#" -import os - -from test import say_hy - -if __name__ == "__main__": - say_hy("dear Ruff contributor") -"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - generated.py:2:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: - - 'extend-select' -> 'lint.extend-select' - "); - }); - - Ok(()) -} - -#[test] -fn extend_per_file_ignores_stdin() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend-select = ["B", "Q"] - -[lint.flake8-quotes] -inline-quotes = "single" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .args(["--stdin-filename", "generated.py"]) - .args(["--extend-per-file-ignores", "generated.py:Q"]) - .arg("-") - .pass_stdin(r#" -import os - -from test import say_hy - -if __name__ == "__main__": - say_hy("dear Ruff contributor") -"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - generated.py:2:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `ruff.toml`: - - 'extend-select' -> 'lint.extend-select' - "); - }); - - Ok(()) -} - -/// Regression test for [#8858](https://github.com/astral-sh/ruff/issues/8858) -#[test] -fn parent_configuration_override() -> Result<()> { - let tempdir = TempDir::new()?; - let root_ruff = tempdir.path().join("ruff.toml"); - fs::write( - root_ruff, - r#" -[lint] -select = ["ALL"] -"#, - )?; - - let sub_dir = tempdir.path().join("subdirectory"); - fs::create_dir(&sub_dir)?; - - let subdirectory_ruff = sub_dir.join("ruff.toml"); - fs::write( - subdirectory_ruff, - r#" -[lint] -ignore = ["D203", "D212"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(sub_dir) - .args(STDIN_BASE_OPTIONS) - , @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - warning: No Python files found under the given path(s) - "); - }); - - Ok(()) -} - -#[test] -fn nonexistent_config_file() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--config", "foo.toml", "."]), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'foo.toml' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - It looks like you were trying to pass a path to a configuration file. - The path `foo.toml` does not point to a configuration file - - For more information, try '--help'. - "); -} - -#[test] -fn config_override_rejected_if_invalid_toml() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--config", "foo = bar", "."]), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'foo = bar' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - The supplied argument is not valid TOML: - - TOML parse error at line 1, column 7 - | - 1 | foo = bar - | ^^^ - string values must be quoted, expected literal string - - For more information, try '--help'. - "); -} - -#[test] -fn too_many_config_files() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_dot_toml = tempdir.path().join("ruff.toml"); - let ruff2_dot_toml = tempdir.path().join("ruff2.toml"); - fs::File::create(&ruff_dot_toml)?; - fs::File::create(&ruff2_dot_toml)?; - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_dot_toml) - .arg("--config") - .arg(&ruff2_dot_toml) - .arg("."), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: You cannot specify more than one configuration file on the command line. - - tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`. - For more information, try `--help`. - "); - }); - Ok(()) -} - -#[test] -fn extend_passed_via_config_argument() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--config", "extend = 'foo.toml'", "."]), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'extend = 'foo.toml'' for '--config ' - - tip: Cannot include `extend` in a --config flag value - - For more information, try '--help'. - "); -} - -#[test] -fn nonexistent_extend_file() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - fs::write( - project_dir.join("ruff.toml"), - r#" -extend = "ruff2.toml" -"#, - )?; - - fs::write( - project_dir.join("ruff2.toml"), - r#" -extend = "ruff3.toml" -"#, - )?; - - insta::with_settings!({ - filters => vec![ - (tempdir_filter(&project_dir).as_str(), "[TMP]/"), - ("The system cannot find the file specified.", "No such file or directory") - ] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(["check"]).current_dir(project_dir), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Failed to load extended configuration `[TMP]/ruff3.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml`) - Cause: Failed to read [TMP]/ruff3.toml - Cause: No such file or directory (os error 2) - "); - }); - - Ok(()) -} - -#[test] -fn circular_extend() -> Result<()> { - let tempdir = TempDir::new()?; - let project_path = dunce::canonicalize(tempdir.path())?; - - fs::write( - project_path.join("ruff.toml"), - r#" -extend = "ruff2.toml" -"#, - )?; - fs::write( - project_path.join("ruff2.toml"), - r#" -extend = "ruff3.toml" -"#, - )?; - fs::write( - project_path.join("ruff3.toml"), - r#" -extend = "ruff.toml" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_path).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check"]) - .current_dir(project_path), - @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Circular configuration detected: `[TMP]/ruff.toml` extends `[TMP]/ruff2.toml` extends `[TMP]/ruff3.toml` extends `[TMP]/ruff.toml` - "); - }); - - Ok(()) -} - -#[test] -fn parse_error_extends() -> Result<()> { - let tempdir = TempDir::new()?; - let project_path = dunce::canonicalize(tempdir.path())?; - - fs::write( - project_path.join("ruff.toml"), - r#" -extend = "ruff2.toml" -"#, - )?; - fs::write( - project_path.join("ruff2.toml"), - r#" -[lint] -select = [E501] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_path).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check"]) - .current_dir(project_path), - @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Failed to load extended configuration `[TMP]/ruff2.toml` (`[TMP]/ruff.toml` extends `[TMP]/ruff2.toml`) - Cause: Failed to parse [TMP]/ruff2.toml - Cause: TOML parse error at line 3, column 11 - | - 3 | select = [E501] - | ^^^^ - string values must be quoted, expected literal string - "); - }); - - Ok(()) -} - -#[test] -fn config_file_and_isolated() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_dot_toml = tempdir.path().join("ruff.toml"); - fs::File::create(&ruff_dot_toml)?; - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_dot_toml) - .arg("--isolated") - .arg("."), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated` - - tip: You cannot specify a configuration file and also specify `--isolated`, - as `--isolated` causes ruff to ignore all configuration files. - For more information, try `--help`. - "); - }); - Ok(()) -} - -#[test] -fn config_override_via_cli() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -line-length = 100 - -[lint] -select = ["I"] - -[lint.isort] -combine-as-imports = true - "#, - )?; - let fixture = r#" -from foo import ( - aaaaaaaaaaaaaaaaaaa, - bbbbbbbbbbb as bbbbbbbbbbbbbbbb, - cccccccccccccccc, - ddddddddddd as ddddddddddddd, - eeeeeeeeeeeeeee, - ffffffffffff as ffffffffffffff, - ggggggggggggg, - hhhhhhh as hhhhhhhhhhh, - iiiiiiiiiiiiii, - jjjjjjjjjjjjj as jjjjjj, -) - -x = "longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" -"#; - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .args(["--config", "line-length=90"]) - .args(["--config", "lint.extend-select=['E501', 'F841']"]) - .args(["--config", "lint.isort.combine-as-imports = false"]) - .arg("-") - .pass_stdin(fixture), @r" - success: false - exit_code: 1 - ----- stdout ----- - -:2:1: I001 [*] Import block is un-sorted or un-formatted - -:15:91: E501 Line too long (97 > 90) - Found 2 errors. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - Ok(()) -} - -#[test] -fn valid_toml_but_nonexistent_option_provided_via_config_argument() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args([".", "--config", "extend-select=['F481']"]), // No such code as F481! - @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'extend-select=['F481']' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - Could not parse the supplied argument as a `ruff.toml` configuration option: - - Unknown rule selector: `F481` - - For more information, try '--help'. - "); -} - -#[test] -fn each_toml_option_requires_a_new_flag_1() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - // commas can't be used to delimit different config overrides; - // you need a new --config flag for each override - .args([".", "--config", "extend-select=['F841'], line-length=90"]), - @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'extend-select=['F841'], line-length=90' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - The supplied argument is not valid TOML: - - TOML parse error at line 1, column 23 - | - 1 | extend-select=['F841'], line-length=90 - | ^ - unexpected key or value, expected newline, `#` - - For more information, try '--help'. - "); -} - -#[test] -fn each_toml_option_requires_a_new_flag_2() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - // spaces *also* can't be used to delimit different config overrides; - // you need a new --config flag for each override - .args([".", "--config", "extend-select=['F841'] line-length=90"]), - @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'extend-select=['F841'] line-length=90' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - The supplied argument is not valid TOML: - - TOML parse error at line 1, column 24 - | - 1 | extend-select=['F841'] line-length=90 - | ^ - unexpected key or value, expected newline, `#` - - For more information, try '--help'. - "); -} - -#[test] -fn value_given_to_table_key_is_not_inline_table_1() { - // https://github.com/astral-sh/ruff/issues/13995 - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args([".", "--config", r#"lint.flake8-pytest-style="csv""#]), - @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'lint.flake8-pytest-style="csv"' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - `lint.flake8-pytest-style` is a table of configuration options. - Did you want to override one of the table's subkeys? - - Possible choices: - - - `lint.flake8-pytest-style.fixture-parentheses` - - `lint.flake8-pytest-style.parametrize-names-type` - - `lint.flake8-pytest-style.parametrize-values-type` - - `lint.flake8-pytest-style.parametrize-values-row-type` - - `lint.flake8-pytest-style.raises-require-match-for` - - `lint.flake8-pytest-style.raises-extend-require-match-for` - - `lint.flake8-pytest-style.mark-parentheses` - - `lint.flake8-pytest-style.warns-require-match-for` - - `lint.flake8-pytest-style.warns-extend-require-match-for` - - For more information, try '--help'. - "#); -} - -#[test] -fn value_given_to_table_key_is_not_inline_table_2() { - // https://github.com/astral-sh/ruff/issues/13995 - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args([".", "--config", r#"lint=123"#]), - @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: invalid value 'lint=123' for '--config ' - - tip: A `--config` flag must either be a path to a `.toml` configuration file - or a TOML ` = ` pair overriding a specific configuration - option - - `lint` is a table of configuration options. - Did you want to override one of the table's subkeys? - - Possible choices: - - - `lint.allowed-confusables` - - `lint.dummy-variable-rgx` - - `lint.extend-ignore` - - `lint.extend-select` - - `lint.extend-fixable` - - `lint.external` - - `lint.fixable` - - `lint.ignore` - - `lint.extend-safe-fixes` - - `lint.extend-unsafe-fixes` - - `lint.ignore-init-module-imports` - - `lint.logger-objects` - - `lint.select` - - `lint.explicit-preview-rules` - - `lint.task-tags` - - `lint.typing-modules` - - `lint.unfixable` - - `lint.per-file-ignores` - - `lint.extend-per-file-ignores` - - `lint.exclude` - - `lint.preview` - - `lint.typing-extensions` - - `lint.future-annotations` - - For more information, try '--help'. - "); -} - -#[test] -fn config_doubly_overridden_via_cli() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -line-length = 100 - -[lint] -select=["E501"] -"#, - )?; - let fixture = "x = 'longer_than_90_charactersssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'"; - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - // The --line-length flag takes priority over both the config file - // and the `--config="line-length=110"` flag, - // despite them both being specified after this flag on the command line: - .args(["--line-length", "90"]) - .arg("--config") - .arg(&ruff_toml) - .args(["--config", "line-length=110"]) - .arg("-") - .pass_stdin(fixture), @r" - success: false - exit_code: 1 - ----- stdout ----- - -:1:91: E501 Line too long (97 > 90) - Found 1 error. - - ----- stderr ----- - "); - Ok(()) -} - -#[test] -fn complex_config_setting_overridden_via_cli() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write(&ruff_toml, "lint.select = ['N801']")?; - let fixture = "class violates_n801: pass"; - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .args(["--config", "lint.per-file-ignores = {'generated.py' = ['N801']}"]) - .args(["--stdin-filename", "generated.py"]) - .arg("-") - .pass_stdin(fixture), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - Ok(()) -} - -#[test] -fn deprecated_config_option_overridden_via_cli() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--config", "select=['N801']", "-"]) - .pass_stdin("class lowercase: ..."), - @r" - success: false - exit_code: 1 - ----- stdout ----- - -:1:7: N801 Class name `lowercase` should use CapWords convention - Found 1 error. - - ----- stderr ----- - warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in your `--config` CLI arguments: - - 'select' -> 'lint.select' - "); -} - -#[test] -fn extension() -> Result<()> { - let tempdir = TempDir::new()?; - - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -include = ["*.ipy"] -"#, - )?; - - fs::write( - tempdir.path().join("main.ipy"), - r#" -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", - "metadata": {}, - "outputs": [], - "source": [ - "import os" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .args(["--extension", "ipy:ipynb"]) - .arg("."), @r" - success: false - exit_code: 1 - ----- stdout ----- - main.ipy:cell 1:1:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -#[test] -fn warn_invalid_noqa_with_no_diagnostics() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--isolated"]) - .arg("--select") - .arg("F401") - .arg("-") - .pass_stdin( - r#" -# ruff: noqa: AAA101 -print("Hello world!") -"# - ) - ); -} - -#[test] -fn file_noqa_external() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -external = ["AAA"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("-") - .pass_stdin(r#" -# flake8: noqa: AAA101, BBB102 -import os -"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - -:3:8: F401 [*] `os` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102 - "); - }); - - 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(()) -} - -/// Expand environment variables in `--config` paths provided via the CLI. -#[test] -fn config_expand() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - ruff_toml, - r#" -[lint] -select = ["F"] -ignore = ["F841"] -"#, - )?; - - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg("${NAME}.toml") - .env("NAME", "ruff") - .arg("-") - .current_dir(tempdir.path()) - .pass_stdin(r#" -import os - -def func(): - x = 1 -"#), @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(()) -} - -/// Per-file selects via ! negation in per-file-ignores -#[test] -fn negated_per_file_ignores() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.per-file-ignores] -"!selected.py" = ["RUF"] -"#, - )?; - let selected = tempdir.path().join("selected.py"); - fs::write(selected, "")?; - let ignored = tempdir.path().join("ignored.py"); - fs::write(ignored, "")?; - - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("--select") - .arg("RUF901") - .current_dir(&tempdir) - , @r" - success: false - exit_code: 1 - ----- stdout ----- - selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix. - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - Ok(()) -} - -#[test] -fn negated_per_file_ignores_absolute() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.per-file-ignores] -"!src/**.py" = ["RUF"] -"#, - )?; - let src_dir = tempdir.path().join("src"); - fs::create_dir(&src_dir)?; - let selected = src_dir.join("selected.py"); - fs::write(selected, "")?; - let ignored = tempdir.path().join("ignored.py"); - fs::write(ignored, "")?; - - insta::with_settings!({filters => vec![(r"\\", "/")]}, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("--select") - .arg("RUF901") - .current_dir(&tempdir) - , @r" - success: false - exit_code: 1 - ----- stdout ----- - src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix. - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - Ok(()) -} - -/// patterns are additive, can't use negative patterns to "un-ignore" -#[test] -fn negated_per_file_ignores_overlap() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.per-file-ignores] -"*.py" = ["RUF"] -"!foo.py" = ["RUF"] -"#, - )?; - let foo_file = tempdir.path().join("foo.py"); - fs::write(foo_file, "")?; - let bar_file = tempdir.path().join("bar.py"); - fs::write(bar_file, "")?; - - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .arg("--select") - .arg("RUF901") - .current_dir(&tempdir) - , @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - Ok(()) -} - -#[test] -fn unused_interaction() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["F"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .args(["--stdin-filename", "test.py"]) - .arg("--fix") - .arg("-") - .pass_stdin(r#" -import os # F401 - -def function(): - import os # F811 - print(os.name) -"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - - import os # F401 - - def function(): - print(os.name) - - ----- stderr ----- - Found 1 error (1 fixed, 0 remaining). - "); - }); - - Ok(()) -} - -#[test] -fn add_noqa() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["RUF015"] -"#, - )?; - - let test_path = tempdir.path().join("noqa.py"); - - fs::write( - &test_path, - r#" -def first_square(): - return [x * x for x in range(20)][0] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .arg(&test_path) - .args(["--add-noqa"]) - .arg("-") - .pass_stdin(r#" - -"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Added 1 noqa directive. - "); - }); - - let test_code = std::fs::read_to_string(&test_path).expect("should read test file"); - - insta::assert_snapshot!(test_code, @r" - def first_square(): - return [x * x for x in range(20)][0] # noqa: RUF015 - "); - - Ok(()) -} - -#[test] -fn add_noqa_multiple_codes() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["ANN001", "ANN201", "ARG001", "D103"] -"#, - )?; - - let test_path = tempdir.path().join("noqa.py"); - - fs::write( - &test_path, - r#" -def unused(x): - pass -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .arg(&test_path) - .arg("--preview") - .args(["--add-noqa"]) - .arg("-") - .pass_stdin(r#" - -"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Added 1 noqa directive. - "); - }); - - let test_code = std::fs::read_to_string(&test_path).expect("should read test file"); - - insta::assert_snapshot!(test_code, @r" - def unused(x): # noqa: ANN001, ANN201, D103 - pass - "); - - Ok(()) -} - -#[test] -fn add_noqa_multiline_diagnostic() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["I"] -"#, - )?; - - let test_path = tempdir.path().join("noqa.py"); - - fs::write( - &test_path, - r#" -import z -import c -import a -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .arg(&test_path) - .args(["--add-noqa"]) - .arg("-") - .pass_stdin(r#" - -"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Added 1 noqa directive. - "); - }); - - let test_code = std::fs::read_to_string(&test_path).expect("should read test file"); - - insta::assert_snapshot!(test_code, @r" - import z # noqa: I001 - import c - import a - "); - - Ok(()) -} - -#[test] -fn add_noqa_existing_noqa() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["ANN001", "ANN201", "ARG001", "D103"] -"#, - )?; - - let test_path = tempdir.path().join("noqa.py"); - - fs::write( - &test_path, - r#" -def unused(x): # noqa: ANN001, ARG001, D103 - pass -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .arg(&test_path) - .arg("--preview") - .args(["--add-noqa"]) - .arg("-") - .pass_stdin(r#" - -"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Added 1 noqa directive. - "); - }); - - let test_code = std::fs::read_to_string(&test_path).expect("should read test file"); - - insta::assert_snapshot!(test_code, @r" - def unused(x): # noqa: ANN001, ANN201, ARG001, D103 - pass - "); - - Ok(()) -} - -#[test] -fn add_noqa_multiline_comment() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["UP031"] -"#, - )?; - - let test_path = tempdir.path().join("noqa.py"); - - fs::write( - &test_path, - r#" -print( - """First line - second line - third line - %s""" - % name -) -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) - .arg(&test_path) - .arg("--preview") - .args(["--add-noqa"]) - .arg("-") - .pass_stdin(r#" - -"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Added 1 noqa directive. - "); - }); - - let test_code = std::fs::read_to_string(&test_path).expect("should read test file"); - - insta::assert_snapshot!(test_code, @r#" - print( - """First line - second line - third line - %s""" # noqa: UP031 - % name - ) - "#); - - Ok(()) -} - -#[test] -fn add_noqa_exclude() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -exclude = ["excluded.py"] -select = ["RUF015"] -"#, - )?; - - let test_path = tempdir.path().join("noqa.py"); - - fs::write( - &test_path, - r#" -def first_square(): - return [x * x for x in range(20)][0] -"#, - )?; - - let exclude_path = tempdir.path().join("excluded.py"); - - fs::write( - &exclude_path, - r#" -def first_square(): - return [x * x for x in range(20)][0] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(tempdir.path()) - .args(STDIN_BASE_OPTIONS) - .args(["--add-noqa"]), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Added 1 noqa directive. - "); - }); - - Ok(()) -} - -/// Regression test for -#[test] -fn add_noqa_parent() -> Result<()> { - let tempdir = TempDir::new()?; - let test_path = tempdir.path().join("noqa.py"); - fs::write( - &test_path, - r#" -from foo import ( # noqa: F401 - bar -) - "#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--add-noqa") - .arg("--select=F401") - .arg("noqa.py") - .current_dir(&tempdir), @r" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Infer `3.11` from `requires-python` in `pyproject.toml`. -#[test] -fn requires_python() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &ruff_toml, - r#"[project] -requires-python = ">= 3.11" - -[tool.ruff.lint] -select = ["UP006"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = ">= 3.8" - -[tool.ruff.lint] -select = ["UP006"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&pyproject_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Infer `3.11` from `requires-python` in `pyproject.toml`. -#[test] -fn requires_python_patch() -> Result<()> { - let tempdir = TempDir::new()?; - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = ">= 3.11.4" - -[tool.ruff.lint] -select = ["UP006"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&pyproject_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Infer `3.11` from `requires-python` in `pyproject.toml`. -#[test] -fn requires_python_equals() -> Result<()> { - let tempdir = TempDir::new()?; - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = "== 3.11" - -[tool.ruff.lint] -select = ["UP006"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&pyproject_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Infer `3.11` from `requires-python` in `pyproject.toml`. -#[test] -fn requires_python_equals_patch() -> Result<()> { - let tempdir = TempDir::new()?; - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = "== 3.11.4" - -[tool.ruff.lint] -select = ["UP006"] -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&pyproject_toml) - .args(["--stdin-filename", "test.py"]) - .arg("-") - .pass_stdin(r#"from typing import List; foo: List[int]"#), @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:31: UP006 [*] Use `list` instead of `List` for type annotation - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// ``` -/// tmp -/// ├── pyproject.toml #<--- no `[tool.ruff]` -/// └── test.py -/// ``` -#[test] -fn requires_python_no_tool() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &ruff_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#"from typing import Union;foo: Union[int, str] = 1"#, - )?; - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .args(["--select","UP007"]) - .arg("test.py") - .arg("-") - .current_dir(project_dir) - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/test.py" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.11 - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.11 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.11 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── pyproject.toml #<--- no `[tool.ruff]` -/// └── test.py -/// ``` -#[test] -fn requires_python_no_tool_preview_enabled() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &ruff_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#"from typing import Union;foo: Union[int, str] = 1"#, - )?; - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--preview") - .arg("--show-settings") - .args(["--select","UP007"]) - .arg("test.py") - .arg("-") - .current_dir(project_dir) - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/test.py" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.pyw", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.11 - linter.per_file_target_version = {} - linter.preview = enabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.11 - formatter.per_file_target_version = {} - formatter.preview = enabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = enabled - analyze.target_version = 3.11 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── pyproject.toml #<--- no `[tool.ruff]` -/// └── test.py -/// ``` -#[test] -fn requires_python_no_tool_target_version_override() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &ruff_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#"from typing import Union;foo: Union[int, str] = 1"#, - )?; - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .args(["--select","UP007"]) - .args(["--target-version","py310"]) - .arg("test.py") - .arg("-") - .current_dir(project_dir) - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/test.py" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.10 - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.10 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.10 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} -/// ``` -/// tmp -/// ├── pyproject.toml #<--- no `[tool.ruff]` -/// └── test.py -/// ``` -#[test] -fn requires_python_no_tool_with_check() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &ruff_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#"from typing import Union;foo: Union[int, str] = 1"#, - )?; - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--select","UP007"]) - .arg(".") - .current_dir(project_dir) - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:31: UP007 [*] Use `X | Y` for type annotations - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "###); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── pyproject.toml #<-- no [tool.ruff] -/// ├── ruff.toml #<-- no `target-version` -/// └── test.py -/// ``` -#[test] -fn requires_python_ruff_toml_no_target_fallback() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#"[lint] -select = ["UP007"] -"#, - )?; - - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#" -from typing import Union;foo: Union[int, str] = 1 -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("test.py") - .arg("--show-settings") - .current_dir(project_dir), @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/test.py" - Settings path: "[TMP]/ruff.toml" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.11 - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.11 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.11 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── pyproject.toml #<-- no [tool.ruff] -/// ├── ruff.toml #<-- no `target-version` -/// └── test.py -/// ``` -#[test] -fn requires_python_ruff_toml_no_target_fallback_check() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#"[lint] -select = ["UP007"] -"#, - )?; - - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#" -from typing import Union;foo: Union[int, str] = 1 -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg(".") - .current_dir(project_dir), @r###" - success: false - exit_code: 1 - ----- stdout ----- - test.py:2:31: UP007 [*] Use `X | Y` for type annotations - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "###); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── foo -/// │ ├── pyproject.toml #<-- no [tool.ruff], no `requires-python` -/// │ └── test.py -/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python` -/// ``` -#[test] -fn requires_python_pyproject_toml_above() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let outer_pyproject = tempdir.path().join("pyproject.toml"); - fs::write( - &outer_pyproject, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let foodir = tempdir.path().join("foo"); - fs::create_dir(foodir)?; - - let inner_pyproject = tempdir.path().join("foo/pyproject.toml"); - fs::write( - &inner_pyproject, - r#"[project] -"#, - )?; - - let testpy = tempdir.path().join("foo/test.py"); - fs::write( - &testpy, - r#" -from typing import Union;foo: Union[int, str] = 1 -"#, - )?; - - let testpy_canon = dunce::canonicalize(testpy)?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"(?m)^foo\\test","foo/test")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .args(["--select","UP007"]) - .arg("foo/test.py") - .current_dir(&project_dir), @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/foo/test.py" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.11 - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.11 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.11 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── foo -/// │ ├── pyproject.toml #<-- has [tool.ruff], no `requires-python` -/// │ └── test.py -/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python` -/// ``` -#[test] -fn requires_python_pyproject_toml_above_with_tool() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let outer_pyproject = tempdir.path().join("pyproject.toml"); - fs::write( - &outer_pyproject, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let foodir = tempdir.path().join("foo"); - fs::create_dir(foodir)?; - - let inner_pyproject = tempdir.path().join("foo/pyproject.toml"); - fs::write( - &inner_pyproject, - r#" -[tool.ruff] -target-version = "py310" -"#, - )?; - - let testpy = tempdir.path().join("foo/test.py"); - fs::write( - &testpy, - r#" -from typing import Union;foo: Union[int, str] = 1 -"#, - )?; - - let testpy_canon = dunce::canonicalize(testpy)?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"foo\\","foo/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .args(["--select","UP007"]) - .arg("foo/test.py") - .current_dir(&project_dir), @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/foo/test.py" - - # General Settings - cache_dir = "[TMP]/foo/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/foo" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/foo" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.10 - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/foo", - "[TMP]/foo/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.10 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.10 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── foo -/// │ ├── pyproject.toml #<-- no [tool.ruff] -/// │ └── test.py -/// └── ruff.toml #<-- no `target-version` -/// ``` -#[test] -fn requires_python_ruff_toml_above() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint] -select = ["UP007"] -"#, - )?; - - let foodir = tempdir.path().join("foo"); - fs::create_dir(foodir)?; - - let pyproject_toml = tempdir.path().join("foo/pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = ">= 3.11" -"#, - )?; - - let testpy = tempdir.path().join("foo/test.py"); - fs::write( - &testpy, - r#" -from typing import Union;foo: Union[int, str] = 1 -"#, - )?; - - let testpy_canon = dunce::canonicalize(testpy)?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .arg("foo/test.py") - .current_dir(&project_dir), @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/foo/test.py" - Settings path: "[TMP]/ruff.toml" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = none - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.9 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.9 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - - insta::with_settings!({ - filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .arg("test.py") - .current_dir(project_dir.join("foo")), @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/foo/test.py" - Settings path: "[TMP]/ruff.toml" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = none - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.9 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.9 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - Ok(()) -} - -/// ``` -/// tmp -/// ├── pyproject.toml <-- requires >=3.10 -/// ├── ruff.toml <--- extends base -/// ├── shared -/// │ └── base_config.toml <-- targets 3.11 -/// └── test.py -/// ``` -#[test] -fn requires_python_extend_from_shared_config() -> Result<()> { - let tempdir = TempDir::new()?; - let project_dir = dunce::canonicalize(tempdir.path())?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -extend = "./shared/base_config.toml" -[lint] -select = ["UP007"] -"#, - )?; - - let shared_dir = tempdir.path().join("shared"); - fs::create_dir(shared_dir)?; - - let pyproject_toml = tempdir.path().join("pyproject.toml"); - fs::write( - &pyproject_toml, - r#"[project] -requires-python = ">= 3.10" -"#, - )?; - - let shared_toml = tempdir.path().join("shared/base_config.toml"); - fs::write( - &shared_toml, - r#" -target-version = "py311" -"#, - )?; - - let testpy = tempdir.path().join("test.py"); - fs::write( - &testpy, - r#" -from typing import Union;foo: Union[int, str] = 1 -"#, - )?; - - let testpy_canon = dunce::canonicalize(testpy)?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--show-settings") - .arg("test.py") - .current_dir(&project_dir), @r#" - success: true - exit_code: 0 - ----- stdout ----- - Resolved settings for: "[TMP]/test.py" - Settings path: "[TMP]/ruff.toml" - - # General Settings - cache_dir = "[TMP]/.ruff_cache" - fix = false - fix_only = false - output_format = concise - show_fixes = false - unsafe_fixes = hint - - # File Resolver Settings - file_resolver.exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "dist", - "node_modules", - "site-packages", - "venv", - ] - file_resolver.extend_exclude = [] - file_resolver.force_exclude = false - file_resolver.include = [ - "*.py", - "*.pyi", - "*.ipynb", - "**/pyproject.toml", - ] - file_resolver.extend_include = [] - file_resolver.respect_gitignore = true - file_resolver.project_root = "[TMP]/" - - # Linter Settings - linter.exclude = [] - linter.project_root = "[TMP]/" - linter.rules.enabled = [ - non-pep604-annotation-union (UP007), - ] - linter.rules.should_fix = [ - non-pep604-annotation-union (UP007), - ] - linter.per_file_ignores = {} - linter.safety_table.forced_safe = [] - linter.safety_table.forced_unsafe = [] - linter.unresolved_target_version = 3.10 - linter.per_file_target_version = {} - linter.preview = disabled - linter.explicit_preview_rules = false - linter.extension = ExtensionMapping({}) - linter.allowed_confusables = [] - linter.builtins = [] - linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ - linter.external = [] - linter.ignore_init_module_imports = true - linter.logger_objects = [] - linter.namespace_packages = [] - linter.src = [ - "[TMP]/", - "[TMP]/src", - ] - linter.tab_size = 4 - linter.line_length = 88 - linter.task_tags = [ - TODO, - FIXME, - XXX, - ] - linter.typing_modules = [] - linter.typing_extensions = true - - # Linter Plugins - linter.flake8_annotations.mypy_init_return = false - linter.flake8_annotations.suppress_dummy_args = false - linter.flake8_annotations.suppress_none_returning = false - linter.flake8_annotations.allow_star_arg_any = false - linter.flake8_annotations.ignore_fully_untyped = false - linter.flake8_bandit.hardcoded_tmp_directory = [ - /tmp, - /var/tmp, - /dev/shm, - ] - linter.flake8_bandit.check_typed_exception = false - linter.flake8_bandit.extend_markup_names = [] - linter.flake8_bandit.allowed_markup_calls = [] - linter.flake8_bugbear.extend_immutable_calls = [] - linter.flake8_builtins.allowed_modules = [] - linter.flake8_builtins.ignorelist = [] - linter.flake8_builtins.strict_checking = false - linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false - linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* - linter.flake8_copyright.author = none - linter.flake8_copyright.min_file_size = 0 - linter.flake8_errmsg.max_string_length = 0 - linter.flake8_gettext.functions_names = [ - _, - gettext, - ngettext, - ] - linter.flake8_implicit_str_concat.allow_multiline = true - linter.flake8_import_conventions.aliases = { - altair = alt, - holoviews = hv, - matplotlib = mpl, - matplotlib.pyplot = plt, - networkx = nx, - numpy = np, - numpy.typing = npt, - pandas = pd, - panel = pn, - plotly.express = px, - polars = pl, - pyarrow = pa, - seaborn = sns, - tensorflow = tf, - tkinter = tk, - xml.etree.ElementTree = ET, - } - linter.flake8_import_conventions.banned_aliases = {} - linter.flake8_import_conventions.banned_from = [] - linter.flake8_pytest_style.fixture_parentheses = false - linter.flake8_pytest_style.parametrize_names_type = tuple - linter.flake8_pytest_style.parametrize_values_type = list - linter.flake8_pytest_style.parametrize_values_row_type = tuple - linter.flake8_pytest_style.raises_require_match_for = [ - BaseException, - Exception, - ValueError, - OSError, - IOError, - EnvironmentError, - socket.error, - ] - linter.flake8_pytest_style.raises_extend_require_match_for = [] - linter.flake8_pytest_style.mark_parentheses = false - linter.flake8_quotes.inline_quotes = double - linter.flake8_quotes.multiline_quotes = double - linter.flake8_quotes.docstring_quotes = double - linter.flake8_quotes.avoid_escape = true - linter.flake8_self.ignore_names = [ - _make, - _asdict, - _replace, - _fields, - _field_defaults, - _name_, - _value_, - ] - linter.flake8_tidy_imports.ban_relative_imports = "parents" - linter.flake8_tidy_imports.banned_api = {} - linter.flake8_tidy_imports.banned_module_level_imports = [] - linter.flake8_type_checking.strict = false - linter.flake8_type_checking.exempt_modules = [ - typing, - typing_extensions, - ] - linter.flake8_type_checking.runtime_required_base_classes = [] - linter.flake8_type_checking.runtime_required_decorators = [] - linter.flake8_type_checking.quote_annotations = false - linter.flake8_unused_arguments.ignore_variadic_names = false - linter.isort.required_imports = [] - linter.isort.combine_as_imports = false - linter.isort.force_single_line = false - linter.isort.force_sort_within_sections = false - linter.isort.detect_same_package = true - linter.isort.case_sensitive = false - linter.isort.force_wrap_aliases = false - linter.isort.force_to_top = [] - linter.isort.known_modules = {} - linter.isort.order_by_type = true - linter.isort.relative_imports_order = furthest_to_closest - linter.isort.single_line_exclusions = [] - linter.isort.split_on_trailing_comma = true - linter.isort.classes = [] - linter.isort.constants = [] - linter.isort.variables = [] - linter.isort.no_lines_before = [] - linter.isort.lines_after_imports = -1 - linter.isort.lines_between_types = 0 - linter.isort.forced_separate = [] - linter.isort.section_order = [ - known { type = future }, - known { type = standard_library }, - known { type = third_party }, - known { type = first_party }, - known { type = local_folder }, - ] - linter.isort.default_section = known { type = third_party } - linter.isort.no_sections = false - linter.isort.from_first = false - linter.isort.length_sort = false - linter.isort.length_sort_straight = false - linter.mccabe.max_complexity = 10 - linter.pep8_naming.ignore_names = [ - setUp, - tearDown, - setUpClass, - tearDownClass, - setUpModule, - tearDownModule, - asyncSetUp, - asyncTearDown, - setUpTestData, - failureException, - longMessage, - maxDiff, - ] - linter.pep8_naming.classmethod_decorators = [] - linter.pep8_naming.staticmethod_decorators = [] - linter.pycodestyle.max_line_length = 88 - linter.pycodestyle.max_doc_length = none - linter.pycodestyle.ignore_overlong_task_comments = false - linter.pyflakes.extend_generics = [] - linter.pyflakes.allowed_unused_imports = [] - linter.pylint.allow_magic_value_types = [ - str, - bytes, - ] - linter.pylint.allow_dunder_method_names = [] - linter.pylint.max_args = 5 - linter.pylint.max_positional_args = 5 - linter.pylint.max_returns = 6 - linter.pylint.max_bool_expr = 5 - linter.pylint.max_branches = 12 - linter.pylint.max_statements = 50 - linter.pylint.max_public_methods = 20 - linter.pylint.max_locals = 15 - linter.pylint.max_nested_blocks = 5 - linter.pyupgrade.keep_runtime_typing = false - linter.ruff.parenthesize_tuple_in_subscript = false - - # Formatter Settings - formatter.exclude = [] - formatter.unresolved_target_version = 3.10 - formatter.per_file_target_version = {} - formatter.preview = disabled - formatter.line_width = 88 - formatter.line_ending = auto - formatter.indent_style = space - formatter.indent_width = 4 - formatter.quote_style = double - formatter.magic_trailing_comma = respect - formatter.docstring_code_format = disabled - formatter.docstring_code_line_width = dynamic - - # Analyze Settings - analyze.exclude = [] - analyze.preview = disabled - analyze.target_version = 3.10 - analyze.string_imports = disabled - analyze.extension = ExtensionMapping({}) - analyze.include_dependencies = {} - - ----- stderr ----- - "#); - }); - - Ok(()) -} - -#[test] -fn checks_notebooks_in_stable() -> anyhow::Result<()> { - let tempdir = TempDir::new()?; - std::fs::write( - tempdir.path().join("main.ipynb"), - r#" -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", - "metadata": {}, - "outputs": [], - "source": [ - "import random" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} -"#, - )?; - - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--select") - .arg("F401") - .current_dir(&tempdir) - , @r" - success: false - exit_code: 1 - ----- stdout ----- - main.ipynb:cell 1:1:8: F401 [*] `random` imported but unused - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - Ok(()) -} - -/// Verify that implicit namespace packages are detected even when they are nested. -/// -/// See: -#[test] -fn nested_implicit_namespace_package() -> Result<()> { - let tempdir = TempDir::new()?; - let root = ChildPath::new(tempdir.path()); - - root.child("foo").child("__init__.py").touch()?; - root.child("foo") - .child("bar") - .child("baz") - .child("__init__.py") - .touch()?; - root.child("foo") - .child("bar") - .child("baz") - .child("bop.py") - .touch()?; - - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--select") - .arg("INP") - .current_dir(&tempdir) - , @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - - insta::with_settings!({filters => vec![(r"\\", "/")]}, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--select") - .arg("INP") - .arg("--preview") - .current_dir(&tempdir) - , @r" - success: false - exit_code: 1 - ----- stdout ----- - foo/bar/baz/__init__.py:1:1: INP001 File `foo/bar/baz/__init__.py` declares a package, but is nested under an implicit namespace package. Add an `__init__.py` to `foo/bar`. - Found 1 error. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -#[test] -fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.flake8-import-conventions.aliases] -"module.name" = "invalid.alias" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - , @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Failed to load configuration `[TMP]/ruff.toml` - Cause: Failed to parse [TMP]/ruff.toml - Cause: TOML parse error at line 3, column 17 - | - 3 | "module.name" = "invalid.alias" - | ^^^^^^^^^^^^^^^ - invalid value: string "invalid.alias", expected a Python identifier - "#);}); - Ok(()) -} - -#[test] -fn flake8_import_convention_invalid_aliases_config_extend_alias_name() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.flake8-import-conventions.extend-aliases] -"module.name" = "__debug__" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - , @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Failed to load configuration `[TMP]/ruff.toml` - Cause: Failed to parse [TMP]/ruff.toml - Cause: TOML parse error at line 3, column 17 - | - 3 | "module.name" = "__debug__" - | ^^^^^^^^^^^ - invalid value: string "__debug__", expected an assignable Python identifier - "#);}); - Ok(()) -} - -#[test] -fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.flake8-import-conventions.aliases] -"module..invalid" = "alias" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - , @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Failed to load configuration `[TMP]/ruff.toml` - Cause: Failed to parse [TMP]/ruff.toml - Cause: TOML parse error at line 3, column 1 - | - 3 | "module..invalid" = "alias" - | ^^^^^^^^^^^^^^^^^ - invalid value: string "module..invalid", expected a sequence of Python identifiers delimited by periods - "#);}); - Ok(()) -} - -#[test] -fn flake8_import_convention_nfkc_normalization() -> Result<()> { - let tempdir = TempDir::new()?; - let ruff_toml = tempdir.path().join("ruff.toml"); - fs::write( - &ruff_toml, - r#" -[lint.flake8-import-conventions.aliases] -"test.module" = "_﹏𝘥𝘦𝘣𝘶𝘨﹏﹏" -"#, - )?; - - insta::with_settings!({ - filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(&ruff_toml) - , @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: Invalid alias for module 'test.module': alias normalizes to '__debug__', which is not allowed. - ");}); - Ok(()) -} - -#[test] -fn flake8_import_convention_unused_aliased_import() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(r#"lint.isort.required-imports = ["import pandas"]"#) - .args(["--select", "I002,ICN001,F401"]) - .args(["--stdin-filename", "test.py"]) - .arg("--unsafe-fixes") - .arg("--fix") - .arg("-") - .pass_stdin("1") - ); -} - -#[test] -fn flake8_import_convention_unused_aliased_import_no_conflict() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#) - .args(["--select", "I002,ICN001,F401"]) - .args(["--stdin-filename", "test.py"]) - .arg("--unsafe-fixes") - .arg("--fix") - .arg("-") - .pass_stdin("1") - ); -} - -// https://github.com/astral-sh/ruff/issues/19842 -#[test] -fn pyupgrade_up026_respects_isort_required_import_fix() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .arg("--isolated") - .arg("check") - .arg("-") - .args(["--select", "I002,UP026"]) - .arg("--config") - .arg(r#"lint.isort.required-imports=["import mock"]"#) - .arg("--fix") - .arg("--no-cache") - .pass_stdin("1\n"), - @r" - success: true - exit_code: 0 - ----- stdout ----- - import mock - 1 - - ----- stderr ----- - Found 1 error (1 fixed, 0 remaining). - " - ); -} - -// https://github.com/astral-sh/ruff/issues/19842 -#[test] -fn pyupgrade_up026_respects_isort_required_import_from_fix() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .arg("--isolated") - .arg("check") - .arg("-") - .args(["--select", "I002,UP026"]) - .arg("--config") - .arg(r#"lint.isort.required-imports = ["from mock import mock"]"#) - .arg("--fix") - .arg("--no-cache") - .pass_stdin("from mock import mock\n"), - @r" - success: true - exit_code: 0 - ----- stdout ----- - from mock import mock - - ----- stderr ----- - All checks passed! - " - ); -} - -// See: https://github.com/astral-sh/ruff/issues/16177 -#[test] -fn flake8_pyi_redundant_none_literal() { - let snippet = r#" -from typing import Literal - -# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements -# but not both, as if both were autofixed it would result in `None | None`, -# which leads to a `TypeError` at runtime. -a: Literal[None,] | Literal[None,] -b: Literal[None] | Literal[None] -c: Literal[None] | Literal[None,] -d: Literal[None,] | Literal[None] -"#; - - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--select", "PYI061"]) - .args(["--stdin-filename", "test.py"]) - .arg("--preview") - .arg("--diff") - .arg("-") - .pass_stdin(snippet), @r" - success: false - exit_code: 1 - ----- stdout ----- - --- test.py - +++ test.py - @@ -4,7 +4,7 @@ - # For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements - # but not both, as if both were autofixed it would result in `None | None`, - # which leads to a `TypeError` at runtime. - -a: Literal[None,] | Literal[None,] - -b: Literal[None] | Literal[None] - -c: Literal[None] | Literal[None,] - -d: Literal[None,] | Literal[None] - +a: None | Literal[None,] - +b: None | Literal[None] - +c: None | Literal[None,] - +d: None | Literal[None] - - - ----- stderr ----- - Would fix 4 errors. - "); -} - -/// Test that private, old-style `TypeVar` generics -/// 1. Get replaced with PEP 695 type parameters (UP046, UP047) -/// 2. Get renamed to remove leading underscores (UP049) -/// 3. Emit a warning that the standalone type variable is now unused (PYI018) -/// 4. Remove the now-unused `Generic` import -#[test] -fn pep695_generic_rename() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--select", "F401,PYI018,UP046,UP047,UP049"]) - .args(["--stdin-filename", "test.py"]) - .arg("--unsafe-fixes") - .arg("--fix") - .arg("--preview") - .arg("--target-version=py312") - .arg("-") - .pass_stdin( - r#" -from typing import Generic, TypeVar -_T = TypeVar("_T") - -class OldStyle(Generic[_T]): - var: _T - -def func(t: _T) -> _T: - x: _T - return x -"# - ), - @r" - success: true - exit_code: 0 - ----- stdout ----- - - - class OldStyle[T]: - var: T - - def func[T](t: T) -> T: - x: T - return x - - ----- stderr ----- - Found 7 errors (7 fixed, 0 remaining). - " - ); -} - -/// Test that we do not rename two different type parameters to the same name -/// in one execution of Ruff (autofixing this to `class Foo[T, T]: ...` would -/// introduce invalid syntax) -#[test] -fn type_parameter_rename_isolation() { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--select", "UP049"]) - .args(["--stdin-filename", "test.py"]) - .arg("--unsafe-fixes") - .arg("--fix") - .arg("--preview") - .arg("--target-version=py312") - .arg("-") - .pass_stdin( - r#" -class Foo[_T, __T]: - pass -"# - ), - @r" - success: false - exit_code: 1 - ----- stdout ----- - - class Foo[T, __T]: - pass - - ----- stderr ----- - test.py:2:14: UP049 Generic class uses private type parameters - Found 2 errors (1 fixed, 1 remaining). - " - ); -} - -/// construct a directory tree with this structure: -/// . -/// ├── abc -/// │ └── __init__.py -/// ├── collections -/// │ ├── __init__.py -/// │ ├── abc -/// │ │ └── __init__.py -/// │ └── foobar -/// │ └── __init__.py -/// ├── foobar -/// │ ├── __init__.py -/// │ ├── abc -/// │ │ └── __init__.py -/// │ └── collections -/// │ ├── __init__.py -/// │ ├── abc -/// │ │ └── __init__.py -/// │ └── foobar -/// │ └── __init__.py -/// ├── ruff.toml -/// └── urlparse -/// └── __init__.py -fn create_a005_module_structure(tempdir: &TempDir) -> Result<()> { - fn create_module(path: &Path) -> Result<()> { - fs::create_dir(path)?; - fs::File::create(path.join("__init__.py"))?; - Ok(()) - } - - let foobar = tempdir.path().join("foobar"); - create_module(&foobar)?; - for base in [&tempdir.path().into(), &foobar] { - for dir in ["abc", "collections"] { - create_module(&base.join(dir))?; - } - create_module(&base.join("collections").join("abc"))?; - create_module(&base.join("collections").join("foobar"))?; - } - create_module(&tempdir.path().join("urlparse"))?; - // also create a ruff.toml to mark the project root - fs::File::create(tempdir.path().join("ruff.toml"))?; - - Ok(()) -} - -/// Test A005 with `strict-checking = true` -#[test] -fn a005_module_shadowing_strict() -> Result<()> { - let tempdir = TempDir::new()?; - create_a005_module_structure(&tempdir)?; - - insta::with_settings!({ - filters => vec![(r"\\", "/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(r#"lint.flake8-builtins.strict-checking = true"#) - .args(["--select", "A005"]) - .current_dir(tempdir.path()), - @r" - success: false - exit_code: 1 - ----- stdout ----- - abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module - collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module - collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module - foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module - foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module - foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module - Found 6 errors. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Test A005 with `strict-checking = false` -#[test] -fn a005_module_shadowing_non_strict() -> Result<()> { - let tempdir = TempDir::new()?; - create_a005_module_structure(&tempdir)?; - - insta::with_settings!({ - filters => vec![(r"\\", "/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--config") - .arg(r#"lint.flake8-builtins.strict-checking = false"#) - .args(["--select", "A005"]) - .current_dir(tempdir.path()), - @r" - success: false - exit_code: 1 - ----- stdout ----- - abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module - collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module - Found 2 errors. - - ----- stderr ----- - "); - - }); - - Ok(()) -} - -/// Test A005 with `strict-checking` unset -/// -/// This should match the non-strict version directly above -#[test] -fn a005_module_shadowing_strict_default() -> Result<()> { - let tempdir = TempDir::new()?; - create_a005_module_structure(&tempdir)?; - - insta::with_settings!({ - filters => vec![(r"\\", "/")] - }, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--select", "A005"]) - .current_dir(tempdir.path()), - @r" - success: false - exit_code: 1 - ----- stdout ----- - abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module - collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module - Found 2 errors. - - ----- stderr ----- - "); - }); - Ok(()) -} - -/// Test that the linter respects per-file-target-version. -#[test] -fn per_file_target_version_linter() { - // without per-file-target-version, there should be one UP046 error - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--target-version", "py312"]) - .args(["--select", "UP046"]) // only triggers on 3.12+ - .args(["--stdin-filename", "test.py"]) - .arg("--preview") - .arg("-") - .pass_stdin(r#" -from typing import Generic, TypeVar - -T = TypeVar("T") - -class A(Generic[T]): - var: T -"#), - @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters - Found 1 error. - No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). - - ----- stderr ----- - " - ); - - // with per-file-target-version, there should be no errors because the new generic syntax is - // unavailable - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--target-version", "py312"]) - .args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#]) - .args(["--select", "UP046"]) // only triggers on 3.12+ - .args(["--stdin-filename", "test.py"]) - .arg("--preview") - .arg("-") - .pass_stdin(r#" -from typing import Generic, TypeVar - -T = TypeVar("T") - -class A(Generic[T]): - var: T -"#), - @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - " - ); -} - -#[test] -fn walrus_before_py38() { - // ok - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--stdin-filename", "test.py"]) - .arg("--target-version=py38") - .arg("-") - .pass_stdin(r#"(x := 1)"#), - @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - " - ); - - // not ok on 3.7 with preview - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--stdin-filename", "test.py"]) - .arg("--target-version=py37") - .arg("--preview") - .arg("-") - .pass_stdin(r#"(x := 1)"#), - @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:1:2: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) - Found 1 error. - - ----- stderr ----- - " - ); -} - -#[test] -fn match_before_py310() { - // ok on 3.10 - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--stdin-filename", "test.py"]) - .arg("--target-version=py310") - .arg("-") - .pass_stdin( - r#" -match 2: - case 1: - print("it's one") -"# - ), - @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - " - ); - - // ok on 3.9 without preview - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--stdin-filename", "test.py"]) - .arg("--target-version=py39") - .arg("-") - .pass_stdin( - r#" -match 2: - case 1: - print("it's one") -"# - ), - @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) - Found 1 error. - - ----- stderr ----- - " - ); - - // syntax error on 3.9 with preview - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--stdin-filename", "test.py"]) - .arg("--target-version=py39") - .arg("--preview") - .arg("-") - .pass_stdin( - r#" -match 2: - case 1: - print("it's one") -"# - ), - @r" - success: false - exit_code: 1 - ----- stdout ----- - test.py:2:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) - Found 1 error. - - ----- stderr ----- - " - ); -} - -/// Regression test for -#[test] -fn cache_syntax_errors() -> Result<()> { - let tempdir = TempDir::new()?; - fs::write(tempdir.path().join("main.py"), "match 2:\n case 1: ...")?; - - let mut cmd = Command::new(get_cargo_bin(BIN_NAME)); - // inline STDIN_BASE_OPTIONS to remove --no-cache - cmd.args(["check", "--output-format", "concise"]) - .arg("--target-version=py39") - .arg("--preview") - .arg("--quiet") // suppress `debug build without --no-cache` warnings - .current_dir(&tempdir); - - assert_cmd_snapshot!( - cmd, - @r" - success: false - exit_code: 1 - ----- stdout ----- - main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) - - ----- stderr ----- - " - ); - - // this should *not* be cached, like normal parse errors - assert_cmd_snapshot!( - cmd, - @r" - success: false - exit_code: 1 - ----- stdout ----- - main.py:1:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) - - ----- stderr ----- - " - ); - - Ok(()) -} - -/// Regression test for with very helpful -/// reproduction repo here: -#[test] -fn cookiecutter_globbing() -> Result<()> { - // This is a simplified directory structure from the repo linked above. The essence of the - // problem is this `{{cookiecutter.repo_name}}` directory containing a config file with a glob. - // The absolute path of the glob contains the glob metacharacters `{{` and `}}` even though the - // user's glob does not. - let tempdir = TempDir::new()?; - let cookiecutter = tempdir.path().join("{{cookiecutter.repo_name}}"); - let cookiecutter_toml = cookiecutter.join("pyproject.toml"); - let tests = cookiecutter.join("tests"); - fs::create_dir_all(&tests)?; - fs::write( - &cookiecutter_toml, - r#"tool.ruff.lint.per-file-ignores = { "tests/*" = ["F811"] }"#, - )?; - // F811 example from the docs to ensure the glob still works - let maintest = tests.join("maintest.py"); - fs::write(maintest, "import foo\nimport bar\nimport foo")?; - - insta::with_settings!({filters => vec![(r"\\", "/")]}, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--select=F811") - .current_dir(tempdir.path()), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - "); - }); - - // after removing the config file with the ignore, F811 applies, so the glob worked above - fs::remove_file(cookiecutter_toml)?; - - insta::with_settings!({filters => vec![(r"\\", "/")]}, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .arg("--select=F811") - .current_dir(tempdir.path()), @r" - success: false - exit_code: 1 - ----- stdout ----- - {{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - - Ok(()) -} - -/// Like the test above but exercises the non-absolute path case in `PerFile::new` -#[test] -fn cookiecutter_globbing_no_project_root() -> Result<()> { - let tempdir = TempDir::new()?; - let tempdir = tempdir.path().join("{{cookiecutter.repo_name}}"); - fs::create_dir(&tempdir)?; - - insta::with_settings!({filters => vec![(r"\\", "/")]}, { - assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) - .current_dir(&tempdir) - .args(STDIN_BASE_OPTIONS) - .args(["--extend-per-file-ignores", "generated.py:Q"]), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - warning: No Python files found under the given path(s) - "); - }); - - Ok(()) -} - -/// Test that semantic syntax errors (1) are emitted, (2) are not cached, (3) don't affect the -/// reporting of normal diagnostics, and (4) are not suppressed by `select = []` (or otherwise -/// disabling all AST-based rules). -#[test] -fn semantic_syntax_errors() -> Result<()> { - let tempdir = TempDir::new()?; - let contents = "[(x := 1) for x in foo]"; - fs::write(tempdir.path().join("main.py"), contents)?; - - let mut cmd = Command::new(get_cargo_bin(BIN_NAME)); - // inline STDIN_BASE_OPTIONS to remove --no-cache - cmd.args(["check", "--output-format", "concise"]) - .arg("--preview") - .arg("--quiet") // suppress `debug build without --no-cache` warnings - .current_dir(&tempdir); - - assert_cmd_snapshot!( - cmd, - @r" - success: false - exit_code: 1 - ----- stdout ----- - main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable - main.py:1:20: F821 Undefined name `foo` - - ----- stderr ----- - " - ); - - // this should *not* be cached, like normal parse errors - assert_cmd_snapshot!( - cmd, - @r" - success: false - exit_code: 1 - ----- stdout ----- - main.py:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable - main.py:1:20: F821 Undefined name `foo` - - ----- stderr ----- - " - ); - - // ensure semantic errors are caught even without AST-based rules selected - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--config", "lint.select = []"]) - .arg("--preview") - .arg("-") - .pass_stdin(contents), - @r" - success: false - exit_code: 1 - ----- stdout ----- - -:1:3: invalid-syntax: assignment expression cannot rebind comprehension variable - Found 1 error. - - ----- stderr ----- - " - ); - - Ok(()) -} - -/// Regression test for . -/// -/// `lint.typing-extensions = false` with Python 3.9 should disable the PYI019 lint because it would -/// try to import `Self` from `typing_extensions` -#[test] -fn combine_typing_extensions_config() { - let contents = " -from typing import TypeVar -T = TypeVar('T') -class Foo: - def f(self: T) -> T: ... -"; - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--config", "lint.typing-extensions = false"]) - .arg("--select=PYI019") - .arg("--target-version=py39") - .arg("-") - .pass_stdin(contents), - @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - " - ); -} - -#[test_case::test_case("concise")] -#[test_case::test_case("full")] -#[test_case::test_case("json")] -#[test_case::test_case("json-lines")] -#[test_case::test_case("junit")] -#[test_case::test_case("grouped")] -#[test_case::test_case("github")] -#[test_case::test_case("gitlab")] -#[test_case::test_case("pylint")] -#[test_case::test_case("rdjson")] -#[test_case::test_case("azure")] -#[test_case::test_case("sarif")] -fn output_format(output_format: &str) -> Result<()> { - const CONTENT: &str = "\ -import os # F401 -x = y # F821 -match 42: # invalid-syntax - case _: ... -"; - - let tempdir = TempDir::new()?; - let input = tempdir.path().join("input.py"); - fs::write(&input, CONTENT)?; - - let snapshot = format!("output_format_{output_format}"); - - let project_dir = dunce::canonicalize(tempdir.path())?; - - insta::with_settings!({ - filters => vec![ - (tempdir_filter(&project_dir).as_str(), "[TMP]/"), - (tempdir_filter(&tempdir).as_str(), "[TMP]/"), - (r#""[^"]+\\?/?input.py"#, r#""[TMP]/input.py"#), - (ruff_linter::VERSION, "[VERSION]"), - ] - }, { - assert_cmd_snapshot!( - snapshot, - Command::new(get_cargo_bin(BIN_NAME)) - .args([ - "check", - "--no-cache", - "--output-format", - output_format, - "--select", - "F401,F821", - "--target-version", - "py39", - "input.py", - ]) - .current_dir(&tempdir), - ); - }); - - Ok(()) -} - -#[test_case::test_case("concise"; "concise_show_fixes")] -#[test_case::test_case("full"; "full_show_fixes")] -#[test_case::test_case("grouped"; "grouped_show_fixes")] -fn output_format_show_fixes(output_format: &str) -> Result<()> { - let tempdir = TempDir::new()?; - let input = tempdir.path().join("input.py"); - fs::write(&input, "import os # F401")?; - - let snapshot = format!("output_format_show_fixes_{output_format}"); - - assert_cmd_snapshot!( - snapshot, - Command::new(get_cargo_bin(BIN_NAME)) - .args([ - "check", - "--no-cache", - "--output-format", - output_format, - "--select", - "F401", - "--fix", - "--show-fixes", - "input.py", - ]) - .current_dir(&tempdir), - ); - - Ok(()) -} - -#[test] -fn up045_nested_optional_flatten_all() { - let contents = "\ -from typing import Optional -nested_optional: Optional[Optional[Optional[str]]] = None -"; - - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(STDIN_BASE_OPTIONS) - .args(["--select", "UP045", "--diff", "--target-version", "py312"]) - .arg("-") - .pass_stdin(contents), - @r" - success: false - exit_code: 1 - ----- stdout ----- - @@ -1,2 +1,2 @@ - from typing import Optional - -nested_optional: Optional[Optional[Optional[str]]] = None - +nested_optional: str | None = None - - - ----- stderr ----- - Would fix 1 error. - ", - ); -} - -#[test] -fn show_fixes_in_full_output_with_preview_enabled() { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check", "--no-cache", "--output-format", "full"]) - .args(["--select", "F401"]) - .arg("--preview") - .arg("-") - .pass_stdin("import math"), - @r" - success: false - exit_code: 1 - ----- stdout ----- - F401 [*] `math` imported but unused - --> -:1:8 - | - 1 | import math - | ^^^^ - | - help: Remove unused import: `math` - - import math - - Found 1 error. - [*] 1 fixable with the `--fix` option. - - ----- stderr ----- - ", - ); -} - -#[test] -fn rule_panic_mixed_results_concise() -> Result<()> { - let tempdir = TempDir::new()?; - - // Create python files - let file_a_path = tempdir.path().join("normal.py"); - let file_b_path = tempdir.path().join("panic.py"); - fs::write(&file_a_path, b"import os")?; - fs::write(&file_b_path, b"print('hello, world!')")?; - - insta::with_settings!({ - filters => vec![ - (tempdir_filter(&tempdir).as_str(), "[TMP]/"), - (r"\\", r"/"), - (r"(Panicked at) [^:]+:\d+:\d+", "$1 ") - ] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check", "--select", "RUF9", "--preview", "--output-format=concise", "--no-cache"]) - .args([file_a_path, file_b_path]), - @r" - success: false - exit_code: 2 - ----- stdout ----- - [TMP]/normal.py:1:1: RUF900 Hey this is a stable test rule. - [TMP]/normal.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix. - [TMP]/normal.py:1:1: RUF902 Hey this is a stable test rule with an unsafe fix. - [TMP]/normal.py:1:1: RUF903 Hey this is a stable test rule with a display only fix. - [TMP]/normal.py:1:1: RUF911 Hey this is a preview test rule. - [TMP]/normal.py:1:1: RUF950 Hey this is a test rule that was redirected from another. - [TMP]/panic.py: panic: Panicked at when checking `[TMP]/panic.py`: `This is a fake panic for testing.` - Found 7 errors. - [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option). - - ----- stderr ----- - error: Panic during linting indicates a bug in Ruff. If you could open an issue at: - - https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D - - ...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative! - "); - }); - Ok(()) -} - -#[test] -fn rule_panic_mixed_results_full() -> Result<()> { - let tempdir = TempDir::new()?; - - // Create python files - let file_a_path = tempdir.path().join("normal.py"); - let file_b_path = tempdir.path().join("panic.py"); - fs::write(&file_a_path, b"import os")?; - fs::write(&file_b_path, b"print('hello, world!')")?; - - insta::with_settings!({ - filters => vec![ - (tempdir_filter(&tempdir).as_str(), "[TMP]/"), - (r"\\", r"/"), - (r"(Panicked at) [^:]+:\d+:\d+", "$1 "), - ] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check", "--select", "RUF9", "--preview", "--output-format=full", "--no-cache"]) - .args([file_a_path, file_b_path]), - @r" - success: false - exit_code: 2 - ----- stdout ----- - RUF900 Hey this is a stable test rule. - --> [TMP]/normal.py:1:1 - - RUF901 [*] Hey this is a stable test rule with a safe fix. - --> [TMP]/normal.py:1:1 - 1 + # fix from stable-test-rule-safe-fix - 2 | import os - - RUF902 Hey this is a stable test rule with an unsafe fix. - --> [TMP]/normal.py:1:1 - - RUF903 Hey this is a stable test rule with a display only fix. - --> [TMP]/normal.py:1:1 - - RUF911 Hey this is a preview test rule. - --> [TMP]/normal.py:1:1 - - RUF950 Hey this is a test rule that was redirected from another. - --> [TMP]/normal.py:1:1 - - panic: Panicked at when checking `[TMP]/panic.py`: `This is a fake panic for testing.` - --> [TMP]/panic.py:1:1 - info: This indicates a bug in Ruff. - info: If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bpanic%5D, we'd be very appreciative! - info: run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information - - Found 7 errors. - [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option). - - ----- stderr ----- - error: Panic during linting indicates a bug in Ruff. If you could open an issue at: - - https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D - - ...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative! - "); - }); - Ok(()) -} - -/// Test that the same rule fires across all supported extensions, but not on unsupported files -#[test] -fn supported_file_extensions() -> Result<()> { - let tempdir = TempDir::new()?; - let inner_dir = tempdir.path().join("src"); - fs::create_dir(&inner_dir)?; - - // Create files of various types - // text file - fs::write(inner_dir.join("thing.txt"), b"hello world\n")?; - // regular python - fs::write( - inner_dir.join("thing.py"), - b"import os\nprint('hello world')\n", - )?; - // python typestub - fs::write( - inner_dir.join("thing.pyi"), - b"import os\nclass foo:\n ...\n", - )?; - // windows gui - fs::write( - inner_dir.join("thing.pyw"), - b"import os\nprint('hello world')\n", - )?; - // cython - fs::write( - inner_dir.join("thing.pyx"), - b"import os\ncdef int add(int a, int b):\n return a + b\n", - )?; - // notebook - fs::write( - inner_dir.join("thing.ipynb"), - r#" -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", - "metadata": {}, - "outputs": [], - "source": [ - "import os" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} -"#, - )?; - - insta::with_settings!({ - filters => vec![ - (tempdir_filter(&tempdir).as_str(), "[TMP]/"), - (r"\\", r"/"), - ] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check", "--select", "F401", "--output-format=concise", "--no-cache"]) - .args([inner_dir]), - @r" - success: false - exit_code: 1 - ----- stdout ----- - [TMP]/src/thing.ipynb:cell 1:1:8: F401 [*] `os` imported but unused - [TMP]/src/thing.py:1:8: F401 [*] `os` imported but unused - [TMP]/src/thing.pyi:1:8: F401 [*] `os` imported but unused - Found 3 errors. - [*] 3 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - Ok(()) -} - -/// Test that the same rule fires across all supported extensions, but not on unsupported files -#[test] -fn supported_file_extensions_preview_enabled() -> Result<()> { - let tempdir = TempDir::new()?; - let inner_dir = tempdir.path().join("src"); - fs::create_dir(&inner_dir)?; - - // Create files of various types - // text file - fs::write(inner_dir.join("thing.txt"), b"hello world\n")?; - // regular python - fs::write( - inner_dir.join("thing.py"), - b"import os\nprint('hello world')\n", - )?; - // python typestub - fs::write( - inner_dir.join("thing.pyi"), - b"import os\nclass foo:\n ...\n", - )?; - // windows gui - fs::write( - inner_dir.join("thing.pyw"), - b"import os\nprint('hello world')\n", - )?; - // cython - fs::write( - inner_dir.join("thing.pyx"), - b"import os\ncdef int add(int a, int b):\n return a + b\n", - )?; - // notebook - fs::write( - inner_dir.join("thing.ipynb"), - r#" -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", - "metadata": {}, - "outputs": [], - "source": [ - "import os" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} -"#, - )?; - - insta::with_settings!({ - filters => vec![ - (tempdir_filter(&tempdir).as_str(), "[TMP]/"), - (r"\\", r"/"), - ] - }, { - assert_cmd_snapshot!( - Command::new(get_cargo_bin(BIN_NAME)) - .args(["check", "--select", "F401", "--preview", "--output-format=concise", "--no-cache"]) - .args([inner_dir]), - @r" - success: false - exit_code: 1 - ----- stdout ----- - [TMP]/src/thing.ipynb:cell 1:1:8: F401 [*] `os` imported but unused - [TMP]/src/thing.py:1:8: F401 [*] `os` imported but unused - [TMP]/src/thing.pyi:1:8: F401 [*] `os` imported but unused - [TMP]/src/thing.pyw:1:8: F401 [*] `os` imported but unused - Found 4 errors. - [*] 4 fixable with the `--fix` option. - - ----- stderr ----- - "); - }); - Ok(()) -}