From d9151b1948ff4ea4e5ab764c58388d1477ca5aa5 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 21 Nov 2023 11:34:21 -0600 Subject: [PATCH] Update `ruff check` and `ruff format` to default to the current directory (#8791) Closes https://github.com/astral-sh/ruff/issues/7347 Closes #3970 via use of `include` We could update examples in our documentation, but I worry since we do not have versioned documentation users on older versions would be confused. Instead, I'll open an issue to track updating use of `ruff check .` in the documentation sometime in the future. --- .../resources/test/fixtures/include-test/a.py | 0 .../resources/test/fixtures/include-test/b.py | 0 .../fixtures/include-test/nested-project/e.py | 0 .../nested-project/pyproject.toml | 2 + .../test/fixtures/include-test/pyproject.toml | 2 + .../fixtures/include-test/subdirectory/c.py | 0 .../fixtures/include-test/subdirectory/d.py | 0 crates/ruff_cli/src/args.rs | 2 + crates/ruff_cli/src/commands/format.rs | 9 +- crates/ruff_cli/src/lib.rs | 40 ++++--- crates/ruff_cli/tests/format.rs | 47 ++++++++ crates/ruff_cli/tests/integration_test.rs | 52 +++++++++ crates/ruff_cli/tests/resolve_files.rs | 101 ++++++++++++++++++ docs/configuration.md | 45 +++++++- docs/formatter.md | 4 + docs/linter.md | 4 + 16 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/a.py create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/b.py create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/nested-project/e.py create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/nested-project/pyproject.toml create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/pyproject.toml create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/c.py create mode 100644 crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/d.py create mode 100644 crates/ruff_cli/tests/resolve_files.rs diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/a.py b/crates/ruff_cli/resources/test/fixtures/include-test/a.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/b.py b/crates/ruff_cli/resources/test/fixtures/include-test/b.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/nested-project/e.py b/crates/ruff_cli/resources/test/fixtures/include-test/nested-project/e.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/nested-project/pyproject.toml b/crates/ruff_cli/resources/test/fixtures/include-test/nested-project/pyproject.toml new file mode 100644 index 0000000000..acbc6447e5 --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/include-test/nested-project/pyproject.toml @@ -0,0 +1,2 @@ +[tool.ruff] +select = [] diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/pyproject.toml b/crates/ruff_cli/resources/test/fixtures/include-test/pyproject.toml new file mode 100644 index 0000000000..fadb2359fb --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/include-test/pyproject.toml @@ -0,0 +1,2 @@ +[tool.ruff] +include = ["a.py", "subdirectory/c.py"] \ No newline at end of file diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/c.py b/crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/c.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/d.py b/crates/ruff_cli/resources/test/fixtures/include-test/subdirectory/d.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index d14c0a9658..6c5ed14456 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -88,6 +88,7 @@ pub enum Command { #[allow(clippy::struct_excessive_bools)] pub struct CheckCommand { /// List of files or directories to check. + #[clap(help = "List of files or directories to check [default: .]")] pub files: Vec, /// Apply fixes to resolve lint violations. /// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes. @@ -363,6 +364,7 @@ pub struct CheckCommand { #[allow(clippy::struct_excessive_bools)] pub struct FormatCommand { /// List of files or directories to format. + #[clap(help = "List of files or directories to format [default: .]")] pub files: Vec, /// Avoid writing any formatted files back; instead, exit with a non-zero status code if any /// files would have been modified, and zero otherwise. diff --git a/crates/ruff_cli/src/commands/format.rs b/crates/ruff_cli/src/commands/format.rs index 1b2e431b8e..697c412163 100644 --- a/crates/ruff_cli/src/commands/format.rs +++ b/crates/ruff_cli/src/commands/format.rs @@ -34,7 +34,7 @@ use crate::args::{CliOverrides, FormatArguments}; use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches}; use crate::panic::{catch_unwind, PanicError}; use crate::resolve::resolve; -use crate::ExitStatus; +use crate::{resolve_default_files, ExitStatus}; #[derive(Debug, Copy, Clone, is_macro::Is)] pub(crate) enum FormatMode { @@ -60,7 +60,7 @@ impl FormatMode { /// Format a set of files, and return the exit status. pub(crate) fn format( - cli: &FormatArguments, + cli: FormatArguments, overrides: &CliOverrides, log_level: LogLevel, ) -> Result { @@ -70,8 +70,9 @@ pub(crate) fn format( overrides, cli.stdin_filename.as_deref(), )?; - let mode = FormatMode::from_cli(cli); - let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, overrides)?; + let mode = FormatMode::from_cli(&cli); + let files = resolve_default_files(cli.files, false); + let (paths, resolver) = python_files_in_path(&files, &pyproject_config, overrides)?; if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 4cb0db0c8b..f8f99505b3 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -101,6 +101,19 @@ fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool { file == Path::new("-") } +/// Returns the default set of files if none are provided, otherwise returns `None`. +fn resolve_default_files(files: Vec, is_stdin: bool) -> Vec { + if files.is_empty() { + if is_stdin { + vec![Path::new("-").to_path_buf()] + } else { + vec![Path::new(".").to_path_buf()] + } + } else { + files + } +} + /// Get the actual value of the `format` desired from either `output_format` /// or `format`, and warn the user if they're using the deprecated form. fn resolve_help_output_format(output_format: HelpFormat, format: Option) -> HelpFormat { @@ -196,7 +209,7 @@ fn format(args: FormatCommand, log_level: LogLevel) -> Result { if is_stdin(&cli.files, cli.stdin_filename.as_deref()) { commands::format_stdin::format_stdin(&cli, &overrides) } else { - commands::format::format(&cli, &overrides, log_level) + commands::format::format(cli, &overrides, log_level) } } @@ -222,17 +235,15 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { }; let stderr_writer = Box::new(BufWriter::new(io::stderr())); + let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref()); + let files = resolve_default_files(cli.files, is_stdin); + if cli.show_settings { - commands::show_settings::show_settings( - &cli.files, - &pyproject_config, - &overrides, - &mut writer, - )?; + commands::show_settings::show_settings(&files, &pyproject_config, &overrides, &mut writer)?; return Ok(ExitStatus::Success); } if cli.show_files { - commands::show_files::show_files(&cli.files, &pyproject_config, &overrides, &mut writer)?; + commands::show_files::show_files(&files, &pyproject_config, &overrides, &mut writer)?; return Ok(ExitStatus::Success); } @@ -295,8 +306,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { if !fix_mode.is_generate() { warn_user!("--fix is incompatible with --add-noqa."); } - let modifications = - commands::add_noqa::add_noqa(&cli.files, &pyproject_config, &overrides)?; + let modifications = commands::add_noqa::add_noqa(&files, &pyproject_config, &overrides)?; if modifications > 0 && log_level >= LogLevel::Default { let s = if modifications == 1 { "" } else { "s" }; #[allow(clippy::print_stderr)] @@ -323,7 +333,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { // Configure the file watcher. let (tx, rx) = channel(); let mut watcher = recommended_watcher(tx)?; - for file in &cli.files { + for file in &files { watcher.watch(file, RecursiveMode::Recursive)?; } if let Some(file) = pyproject_config.path.as_ref() { @@ -335,7 +345,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { printer.write_to_user("Starting linter in watch mode...\n"); let messages = commands::check::check( - &cli.files, + &files, &pyproject_config, &overrides, cache.into(), @@ -368,7 +378,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { printer.write_to_user("File change detected...\n"); let messages = commands::check::check( - &cli.files, + &files, &pyproject_config, &overrides, cache.into(), @@ -382,8 +392,6 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { } } } else { - let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref()); - // Generate lint violations. let diagnostics = if is_stdin { commands::check_stdin::check_stdin( @@ -395,7 +403,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result { )? } else { commands::check::check( - &cli.files, + &files, &pyproject_config, &overrides, cache.into(), diff --git a/crates/ruff_cli/tests/format.rs b/crates/ruff_cli/tests/format.rs index c5c3995633..79a7cf942d 100644 --- a/crates/ruff_cli/tests/format.rs +++ b/crates/ruff_cli/tests/format.rs @@ -43,6 +43,53 @@ if condition: "###); } +#[test] +fn default_files() -> Result<()> { + let tempdir = TempDir::new()?; + fs::write( + tempdir.path().join("foo.py"), + r#" +foo = "needs formatting" +"#, + )?; + fs::write( + tempdir.path().join("bar.py"), + r#" +bar = "needs formatting" +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["format", "--isolated", "--no-cache", "--check"]).current_dir(tempdir.path()), @r###" + success: false + exit_code: 1 + ----- stdout ----- + Would reformat: bar.py + Would reformat: foo.py + 2 files would be reformatted + + ----- stderr ----- + "###); + + Ok(()) +} + +#[test] +fn format_warn_stdin_filename_with_files() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["format", "--stdin-filename", "foo.py"]) + .arg("foo.py") + .pass_stdin("foo = 1"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + foo = 1 + + ----- stderr ----- + warning: Ignoring file foo.py in favor of standard input. + "###); +} + #[test] fn format_options() -> Result<()> { let tempdir = TempDir::new()?; diff --git a/crates/ruff_cli/tests/integration_test.rs b/crates/ruff_cli/tests/integration_test.rs index 59c372aac1..bcba6b255b 100644 --- a/crates/ruff_cli/tests/integration_test.rs +++ b/crates/ruff_cli/tests/integration_test.rs @@ -74,6 +74,57 @@ fn stdin_filename() { "###); } +#[test] +fn check_default_files() -> Result<()> { + let tempdir = TempDir::new()?; + fs::write( + tempdir.path().join("foo.py"), + r#" +import foo # unused import +"#, + )?; + fs::write( + tempdir.path().join("bar.py"), + r#" +import bar # unused import +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--isolated", "--no-cache", "--select", "F401"]).current_dir(tempdir.path()), @r###" + success: false + exit_code: 1 + ----- stdout ----- + bar.py:2:8: F401 [*] `bar` imported but unused + foo.py:2:8: F401 [*] `foo` imported but unused + Found 2 errors. + [*] 2 fixable with the `--fix` option. + + ----- stderr ----- + "###); + + Ok(()) +} + +#[test] +fn check_warn_stdin_filename_with_files() { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--stdin-filename", "F401.py"]) + .arg("foo.py") + .pass_stdin("import os\n"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + F401.py:1:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + warning: Ignoring file foo.py in favor of standard input. + "###); +} + /// Raise `TCH` errors in `.py` files ... #[test] fn stdin_source_type_py() { @@ -320,6 +371,7 @@ fn stdin_fix_jupyter() { Found 2 errors (2 fixed, 0 remaining). "###); } + #[test] fn stdin_override_parser_ipynb() { let args = ["--extension", "py:ipynb", "--stdin-filename", "Jupyter.py"]; diff --git a/crates/ruff_cli/tests/resolve_files.rs b/crates/ruff_cli/tests/resolve_files.rs new file mode 100644 index 0000000000..b9f75a8760 --- /dev/null +++ b/crates/ruff_cli/tests/resolve_files.rs @@ -0,0 +1,101 @@ +#![cfg(not(target_family = "wasm"))] + +use std::path::Path; +use std::process::Command; +use std::str; + +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; +const BIN_NAME: &str = "ruff"; + +#[cfg(not(target_os = "windows"))] +const TEST_FILTERS: &[(&str, &str)] = &[(".*/resources/test/fixtures/", "[BASEPATH]/")]; +#[cfg(target_os = "windows")] +const TEST_FILTERS: &[(&str, &str)] = &[ + (r".*\\resources\\test\\fixtures\\", "[BASEPATH]\\"), + (r"\\", "/"), +]; + +#[test] +fn check_project_include_defaults() { + // Defaults to checking the current working directory + // + // The test directory includes: + // - A pyproject.toml which specifies an include + // - A nested pyproject.toml which has a Ruff section + // + // The nested project should all be checked instead of respecting the parent includes + + insta::with_settings!({ + filters => TEST_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--show-files"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [BASEPATH]/include-test/a.py + [BASEPATH]/include-test/nested-project/e.py + [BASEPATH]/include-test/nested-project/pyproject.toml + [BASEPATH]/include-test/subdirectory/c.py + + ----- stderr ----- + "###); + }); +} + +#[test] +fn check_project_respects_direct_paths() { + // Given a direct path not included in the project `includes`, it should be checked + + insta::with_settings!({ + filters => TEST_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--show-files", "b.py"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [BASEPATH]/include-test/b.py + + ----- stderr ----- + "###); + }); +} + +#[test] +fn check_project_respects_subdirectory_includes() { + // Given a direct path to a subdirectory, the include should be respected + + insta::with_settings!({ + filters => TEST_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--show-files", "subdirectory"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [BASEPATH]/include-test/subdirectory/c.py + + ----- stderr ----- + "###); + }); +} + +#[test] +fn check_project_from_project_subdirectory_respects_includes() { + // Run from a project subdirectory, the include specified in the parent directory should be respected + + insta::with_settings!({ + filters => TEST_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["check", "--show-files"]).current_dir(Path::new("./resources/test/fixtures/include-test/subdirectory")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [BASEPATH]/include-test/subdirectory/c.py + + ----- stderr ----- + "###); + }); +} diff --git a/docs/configuration.md b/docs/configuration.md index bc950ad3ac..85071a439f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -299,6 +299,47 @@ By default, Ruff will also skip any files that are omitted via `.ignore`, `.giti Files that are passed to `ruff` directly are always analyzed, regardless of the above criteria. For example, `ruff check /path/to/excluded/file.py` will always lint `file.py`. +### Default inclusions + +By default, Ruff will discover files matching `*.py`, `*.ipy`, or `pyproject.toml`. + +To lint or format files with additional file extensions, use the [`extend-include`](settings.md#extend-include) setting. + +=== "pyproject.toml" + + ```toml + [tool.ruff.lint] + extend-include = ["*.ipynb"] + ``` + +=== "ruff.toml" + + ```toml + [lint] + extend-include = ["*.ipynb"] + ``` + +You can also change the default selection using the [`include`](settings.md#include) setting. + + +=== "pyproject.toml" + + ```toml + [tool.ruff.lint] + include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py"] + ``` + +=== "ruff.toml" + + ```toml + [lint] + include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py"] + ``` + +!!! warning + Paths provided to `include` _must_ match files. For example, `include = ["src"]` will fail since it +matches a directory. + ## Jupyter Notebook discovery Ruff has built-in support for [Jupyter Notebooks](https://jupyter.org/). @@ -422,7 +463,7 @@ Run Ruff on the given files or directories (default) Usage: ruff check [OPTIONS] [FILES]... Arguments: - [FILES]... List of files or directories to check + [FILES]... List of files or directories to check [default: .] Options: --fix @@ -518,7 +559,7 @@ Run the Ruff formatter on the given files or directories Usage: ruff format [OPTIONS] [FILES]... Arguments: - [FILES]... List of files or directories to format + [FILES]... List of files or directories to format [default: .] Options: --check diff --git a/docs/formatter.md b/docs/formatter.md index caa7459e55..bbbc944c00 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -19,6 +19,10 @@ and instead exit with a non-zero status code upon detecting any unformatted file For the full list of supported options, run `ruff format --help`. +!!! note + As of Ruff v0.1.7 the `ruff format` command uses the current working directory (`.`) as the default path to format. + See [the file discovery documentation](configuration.md#python-file-discovery) for details. + ## Philosophy The initial goal of the Ruff formatter is _not_ to innovate on code style, but rather, to innovate diff --git a/docs/linter.md b/docs/linter.md index 71c5d04ef2..01d258d98c 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -18,6 +18,10 @@ ruff check . --watch # Lint all files in the current directory, and re-lint on For the full list of supported options, run `ruff check --help`. +!!! note + As of Ruff v0.1.7 the `ruff check` command uses the current working directory (`.`) as the default path to check. + See [the file discovery documentation](configuration.md#python-file-discovery) for details. + ## Rule selection The set of enabled rules is controlled via the [`select`](settings.md#select),