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.
This commit is contained in:
Zanie Blue 2023-11-21 11:34:21 -06:00 committed by GitHub
parent b61ce7fa46
commit d9151b1948
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 286 additions and 22 deletions

View file

@ -0,0 +1,2 @@
[tool.ruff]
select = []

View file

@ -0,0 +1,2 @@
[tool.ruff]
include = ["a.py", "subdirectory/c.py"]

View file

@ -88,6 +88,7 @@ pub enum Command {
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct CheckCommand { pub struct CheckCommand {
/// List of files or directories to check. /// List of files or directories to check.
#[clap(help = "List of files or directories to check [default: .]")]
pub files: Vec<PathBuf>, pub files: Vec<PathBuf>,
/// Apply fixes to resolve lint violations. /// Apply fixes to resolve lint violations.
/// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes. /// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes.
@ -363,6 +364,7 @@ pub struct CheckCommand {
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct FormatCommand { pub struct FormatCommand {
/// List of files or directories to format. /// List of files or directories to format.
#[clap(help = "List of files or directories to format [default: .]")]
pub files: Vec<PathBuf>, pub files: Vec<PathBuf>,
/// Avoid writing any formatted files back; instead, exit with a non-zero status code if any /// Avoid writing any formatted files back; instead, exit with a non-zero status code if any
/// files would have been modified, and zero otherwise. /// files would have been modified, and zero otherwise.

View file

@ -34,7 +34,7 @@ use crate::args::{CliOverrides, FormatArguments};
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches}; use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
use crate::panic::{catch_unwind, PanicError}; use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve; use crate::resolve::resolve;
use crate::ExitStatus; use crate::{resolve_default_files, ExitStatus};
#[derive(Debug, Copy, Clone, is_macro::Is)] #[derive(Debug, Copy, Clone, is_macro::Is)]
pub(crate) enum FormatMode { pub(crate) enum FormatMode {
@ -60,7 +60,7 @@ impl FormatMode {
/// Format a set of files, and return the exit status. /// Format a set of files, and return the exit status.
pub(crate) fn format( pub(crate) fn format(
cli: &FormatArguments, cli: FormatArguments,
overrides: &CliOverrides, overrides: &CliOverrides,
log_level: LogLevel, log_level: LogLevel,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -70,8 +70,9 @@ pub(crate) fn format(
overrides, overrides,
cli.stdin_filename.as_deref(), cli.stdin_filename.as_deref(),
)?; )?;
let mode = FormatMode::from_cli(cli); let mode = FormatMode::from_cli(&cli);
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, overrides)?; let files = resolve_default_files(cli.files, false);
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, overrides)?;
if paths.is_empty() { if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)"); warn_user_once!("No Python files found under the given path(s)");

View file

@ -101,6 +101,19 @@ fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool {
file == Path::new("-") file == Path::new("-")
} }
/// Returns the default set of files if none are provided, otherwise returns `None`.
fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
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` /// 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. /// or `format`, and warn the user if they're using the deprecated form.
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat { fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
@ -196,7 +209,7 @@ fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) { if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
commands::format_stdin::format_stdin(&cli, &overrides) commands::format_stdin::format_stdin(&cli, &overrides)
} else { } 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<ExitStatus> {
}; };
let stderr_writer = Box::new(BufWriter::new(io::stderr())); 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 { if cli.show_settings {
commands::show_settings::show_settings( commands::show_settings::show_settings(&files, &pyproject_config, &overrides, &mut writer)?;
&cli.files,
&pyproject_config,
&overrides,
&mut writer,
)?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
if cli.show_files { 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); return Ok(ExitStatus::Success);
} }
@ -295,8 +306,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
if !fix_mode.is_generate() { if !fix_mode.is_generate() {
warn_user!("--fix is incompatible with --add-noqa."); warn_user!("--fix is incompatible with --add-noqa.");
} }
let modifications = let modifications = commands::add_noqa::add_noqa(&files, &pyproject_config, &overrides)?;
commands::add_noqa::add_noqa(&cli.files, &pyproject_config, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default { if modifications > 0 && log_level >= LogLevel::Default {
let s = if modifications == 1 { "" } else { "s" }; let s = if modifications == 1 { "" } else { "s" };
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
@ -323,7 +333,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
// Configure the file watcher. // Configure the file watcher.
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = recommended_watcher(tx)?; let mut watcher = recommended_watcher(tx)?;
for file in &cli.files { for file in &files {
watcher.watch(file, RecursiveMode::Recursive)?; watcher.watch(file, RecursiveMode::Recursive)?;
} }
if let Some(file) = pyproject_config.path.as_ref() { if let Some(file) = pyproject_config.path.as_ref() {
@ -335,7 +345,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
printer.write_to_user("Starting linter in watch mode...\n"); printer.write_to_user("Starting linter in watch mode...\n");
let messages = commands::check::check( let messages = commands::check::check(
&cli.files, &files,
&pyproject_config, &pyproject_config,
&overrides, &overrides,
cache.into(), cache.into(),
@ -368,7 +378,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
printer.write_to_user("File change detected...\n"); printer.write_to_user("File change detected...\n");
let messages = commands::check::check( let messages = commands::check::check(
&cli.files, &files,
&pyproject_config, &pyproject_config,
&overrides, &overrides,
cache.into(), cache.into(),
@ -382,8 +392,6 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
} }
} }
} else { } else {
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
// Generate lint violations. // Generate lint violations.
let diagnostics = if is_stdin { let diagnostics = if is_stdin {
commands::check_stdin::check_stdin( commands::check_stdin::check_stdin(
@ -395,7 +403,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
)? )?
} else { } else {
commands::check::check( commands::check::check(
&cli.files, &files,
&pyproject_config, &pyproject_config,
&overrides, &overrides,
cache.into(), cache.into(),

View file

@ -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] #[test]
fn format_options() -> Result<()> { fn format_options() -> Result<()> {
let tempdir = TempDir::new()?; let tempdir = TempDir::new()?;

View file

@ -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 ... /// Raise `TCH` errors in `.py` files ...
#[test] #[test]
fn stdin_source_type_py() { fn stdin_source_type_py() {
@ -320,6 +371,7 @@ fn stdin_fix_jupyter() {
Found 2 errors (2 fixed, 0 remaining). Found 2 errors (2 fixed, 0 remaining).
"###); "###);
} }
#[test] #[test]
fn stdin_override_parser_ipynb() { fn stdin_override_parser_ipynb() {
let args = ["--extension", "py:ipynb", "--stdin-filename", "Jupyter.py"]; let args = ["--extension", "py:ipynb", "--stdin-filename", "Jupyter.py"];

View file

@ -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 -----
"###);
});
}

View file

@ -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. 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`. 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 ## Jupyter Notebook discovery
Ruff has built-in support for [Jupyter Notebooks](https://jupyter.org/). 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]... Usage: ruff check [OPTIONS] [FILES]...
Arguments: Arguments:
[FILES]... List of files or directories to check [FILES]... List of files or directories to check [default: .]
Options: Options:
--fix --fix
@ -518,7 +559,7 @@ Run the Ruff formatter on the given files or directories
Usage: ruff format [OPTIONS] [FILES]... Usage: ruff format [OPTIONS] [FILES]...
Arguments: Arguments:
[FILES]... List of files or directories to format [FILES]... List of files or directories to format [default: .]
Options: Options:
--check --check

View file

@ -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`. 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 ## Philosophy
The initial goal of the Ruff formatter is _not_ to innovate on code style, but rather, to innovate The initial goal of the Ruff formatter is _not_ to innovate on code style, but rather, to innovate

View file

@ -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`. 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 ## Rule selection
The set of enabled rules is controlled via the [`select`](settings.md#select), The set of enabled rules is controlled via the [`select`](settings.md#select),