mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
Add --extension
support to the formatter (#9483)
## Summary We added `--extension` to `ruff check`, but it's equally applicable to `ruff format`. Closes https://github.com/astral-sh/ruff/issues/9482. Resolves https://github.com/astral-sh/ruff/discussions/9481. ## Test Plan `cargo test`
This commit is contained in:
parent
d16c4a2d25
commit
3261d16e61
11 changed files with 221 additions and 51 deletions
|
@ -290,6 +290,10 @@ pub struct CheckCommand {
|
|||
/// The name of the file when passing it through stdin.
|
||||
#[arg(long, help_heading = "Miscellaneous")]
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For
|
||||
/// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extension: Option<Vec<ExtensionPair>>,
|
||||
/// Exit with status code "0", even upon detecting lint violations.
|
||||
#[arg(
|
||||
short,
|
||||
|
@ -352,9 +356,6 @@ pub struct CheckCommand {
|
|||
conflicts_with = "watch",
|
||||
)]
|
||||
pub show_settings: bool,
|
||||
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]).
|
||||
#[arg(long, value_delimiter = ',', hide = true)]
|
||||
pub extension: Option<Vec<ExtensionPair>>,
|
||||
/// Dev-only argument to show fixes
|
||||
#[arg(long, hide = true)]
|
||||
pub ecosystem_ci: bool,
|
||||
|
@ -423,6 +424,10 @@ pub struct FormatCommand {
|
|||
/// The name of the file when passing it through stdin.
|
||||
#[arg(long, help_heading = "Miscellaneous")]
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For
|
||||
/// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extension: Option<Vec<ExtensionPair>>,
|
||||
/// The minimum Python version that should be supported.
|
||||
#[arg(long, value_enum)]
|
||||
pub target_version: Option<PythonVersion>,
|
||||
|
@ -571,6 +576,7 @@ impl FormatCommand {
|
|||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||
target_version: self.target_version,
|
||||
cache_dir: self.cache_dir,
|
||||
extension: self.extension,
|
||||
|
||||
// Unsupported on the formatter CLI, but required on `Overrides`.
|
||||
..CliOverrides::default()
|
||||
|
@ -739,7 +745,7 @@ impl ConfigurationTransformer for CliOverrides {
|
|||
config.target_version = Some(*target_version);
|
||||
}
|
||||
if let Some(extension) = &self.extension {
|
||||
config.lint.extension = Some(extension.clone().into_iter().collect());
|
||||
config.extension = Some(extension.iter().cloned().collect());
|
||||
}
|
||||
|
||||
config
|
||||
|
|
|
@ -106,13 +106,19 @@ pub(crate) fn format(
|
|||
match entry {
|
||||
Ok(resolved_file) => {
|
||||
let path = resolved_file.path();
|
||||
let SourceType::Python(source_type) = SourceType::from(&path) else {
|
||||
// Ignore any non-Python files.
|
||||
return None;
|
||||
};
|
||||
|
||||
let settings = resolver.resolve(path);
|
||||
|
||||
let source_type = match settings.formatter.extension.get(path) {
|
||||
None => match SourceType::from(path) {
|
||||
SourceType::Python(source_type) => source_type,
|
||||
SourceType::Toml(_) => {
|
||||
// Ignore any non-Python files.
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Some(language) => PySourceType::from(language),
|
||||
};
|
||||
|
||||
// Ignore files that are excluded from formatting
|
||||
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
|
||||
&& match_exclusion(
|
||||
|
|
|
@ -53,16 +53,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
|||
}
|
||||
|
||||
let path = cli.stdin_filename.as_deref();
|
||||
let settings = &resolver.base_settings().formatter;
|
||||
|
||||
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
|
||||
if mode.is_write() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
let source_type = match path.and_then(|path| settings.extension.get(path)) {
|
||||
None => match path.map(SourceType::from).unwrap_or_default() {
|
||||
SourceType::Python(source_type) => source_type,
|
||||
SourceType::Toml(_) => {
|
||||
if mode.is_write() {
|
||||
parrot_stdin()?;
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
},
|
||||
Some(language) => PySourceType::from(language),
|
||||
};
|
||||
|
||||
// Format the file.
|
||||
match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
|
||||
match format_source_code(path, settings, source_type, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
|
|
|
@ -17,7 +17,7 @@ use ruff_linter::logging::DisplayParseError;
|
|||
use ruff_linter::message::Message;
|
||||
use ruff_linter::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff_linter::registry::AsRule;
|
||||
use ruff_linter::settings::types::{ExtensionMapping, UnsafeFixes};
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{flags, LinterSettings};
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::{fs, IOError, SyntaxError};
|
||||
|
@ -179,11 +179,6 @@ impl AddAssign for FixMap {
|
|||
}
|
||||
}
|
||||
|
||||
fn override_source_type(path: Option<&Path>, extension: &ExtensionMapping) -> Option<PySourceType> {
|
||||
let ext = path?.extension()?.to_str()?;
|
||||
extension.get(ext).map(PySourceType::from)
|
||||
}
|
||||
|
||||
/// Lint the source code at the given `Path`.
|
||||
pub(crate) fn lint_path(
|
||||
path: &Path,
|
||||
|
@ -228,7 +223,7 @@ pub(crate) fn lint_path(
|
|||
|
||||
debug!("Checking: {}", path.display());
|
||||
|
||||
let source_type = match override_source_type(Some(path), &settings.extension) {
|
||||
let source_type = match settings.extension.get(path).map(PySourceType::from) {
|
||||
Some(source_type) => source_type,
|
||||
None => match SourceType::from(path) {
|
||||
SourceType::Toml(TomlSourceType::Pyproject) => {
|
||||
|
@ -398,15 +393,14 @@ pub(crate) fn lint_stdin(
|
|||
fix_mode: flags::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
// TODO(charlie): Support `pyproject.toml`.
|
||||
let source_type = if let Some(source_type) =
|
||||
override_source_type(path, &settings.linter.extension)
|
||||
{
|
||||
source_type
|
||||
} else {
|
||||
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
|
||||
return Ok(Diagnostics::default());
|
||||
};
|
||||
source_type
|
||||
let source_type = match path.and_then(|path| settings.linter.extension.get(path)) {
|
||||
None => match path.map(SourceType::from).unwrap_or_default() {
|
||||
SourceType::Python(source_type) => source_type,
|
||||
SourceType::Toml(_) => {
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
},
|
||||
Some(language) => PySourceType::from(language),
|
||||
};
|
||||
|
||||
// Extract the sources from the file.
|
||||
|
|
|
@ -1468,3 +1468,73 @@ fn test_notebook_trailing_semicolon() {
|
|||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[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": [
|
||||
"x=1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("format")
|
||||
.arg("--no-cache")
|
||||
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
|
||||
.args(["--extension", "ipy:ipynb"])
|
||||
.arg("."), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -436,3 +436,75 @@ ignore = ["D203", "D212"]
|
|||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
.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(())
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ pub mod types;
|
|||
#[derive(Debug, CacheKey)]
|
||||
pub struct LinterSettings {
|
||||
pub exclude: FilePatternSet,
|
||||
pub extension: ExtensionMapping,
|
||||
pub project_root: PathBuf,
|
||||
|
||||
pub rules: RuleTable,
|
||||
|
@ -50,7 +51,6 @@ pub struct LinterSettings {
|
|||
pub target_version: PythonVersion,
|
||||
pub preview: PreviewMode,
|
||||
pub explicit_preview_rules: bool,
|
||||
pub extension: ExtensionMapping,
|
||||
|
||||
// Rule-specific settings
|
||||
pub allowed_confusables: FxHashSet<char>,
|
||||
|
|
|
@ -388,9 +388,10 @@ pub struct ExtensionMapping {
|
|||
}
|
||||
|
||||
impl ExtensionMapping {
|
||||
/// Return the [`Language`] for the given extension.
|
||||
pub fn get(&self, extension: &str) -> Option<Language> {
|
||||
self.mapping.get(extension).copied()
|
||||
/// Return the [`Language`] for the given file.
|
||||
pub fn get(&self, path: &Path) -> Option<Language> {
|
||||
let ext = path.extension()?.to_str()?;
|
||||
self.mapping.get(ext).copied()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ pub struct Configuration {
|
|||
pub output_format: Option<SerializationFormat>,
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub required_version: Option<Version>,
|
||||
pub extension: Option<ExtensionMapping>,
|
||||
pub show_fixes: Option<bool>,
|
||||
pub show_source: Option<bool>,
|
||||
|
||||
|
@ -174,6 +175,7 @@ impl Configuration {
|
|||
|
||||
let formatter = FormatterSettings {
|
||||
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
|
||||
extension: self.extension.clone().unwrap_or_default(),
|
||||
preview: format_preview,
|
||||
target_version: match target_version {
|
||||
PythonVersion::Py37 => ruff_python_formatter::PythonVersion::Py37,
|
||||
|
@ -241,7 +243,7 @@ impl Configuration {
|
|||
linter: LinterSettings {
|
||||
rules: lint.as_rule_table(lint_preview),
|
||||
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
|
||||
extension: lint.extension.unwrap_or_default(),
|
||||
extension: self.extension.unwrap_or_default(),
|
||||
preview: lint_preview,
|
||||
target_version,
|
||||
project_root: project_root.to_path_buf(),
|
||||
|
@ -496,6 +498,9 @@ impl Configuration {
|
|||
.map(|src| resolve_src(&src, project_root))
|
||||
.transpose()?,
|
||||
target_version: options.target_version,
|
||||
// `--extension` is a hidden command-line argument that isn't supported in configuration
|
||||
// files at present.
|
||||
extension: None,
|
||||
|
||||
lint: LintConfiguration::from_options(lint, project_root)?,
|
||||
format: FormatConfiguration::from_options(
|
||||
|
@ -538,6 +543,7 @@ impl Configuration {
|
|||
src: self.src.or(config.src),
|
||||
target_version: self.target_version.or(config.target_version),
|
||||
preview: self.preview.or(config.preview),
|
||||
extension: self.extension.or(config.extension),
|
||||
|
||||
lint: self.lint.combine(config.lint),
|
||||
format: self.format.combine(config.format),
|
||||
|
@ -549,7 +555,6 @@ impl Configuration {
|
|||
pub struct LintConfiguration {
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub extension: Option<ExtensionMapping>,
|
||||
|
||||
// Rule selection
|
||||
pub extend_per_file_ignores: Vec<PerFileIgnore>,
|
||||
|
@ -616,9 +621,6 @@ impl LintConfiguration {
|
|||
.chain(options.common.extend_unfixable.into_iter().flatten())
|
||||
.collect();
|
||||
Ok(LintConfiguration {
|
||||
// `--extension` is a hidden command-line argument that isn't supported in configuration
|
||||
// files at present.
|
||||
extension: None,
|
||||
exclude: options.exclude.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
|
@ -954,7 +956,6 @@ impl LintConfiguration {
|
|||
Self {
|
||||
exclude: self.exclude.or(config.exclude),
|
||||
preview: self.preview.or(config.preview),
|
||||
extension: self.extension.or(config.extension),
|
||||
rule_selections: config
|
||||
.rule_selections
|
||||
.into_iter()
|
||||
|
@ -1031,6 +1032,7 @@ impl LintConfiguration {
|
|||
pub struct FormatConfiguration {
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub preview: Option<PreviewMode>,
|
||||
pub extension: Option<ExtensionMapping>,
|
||||
|
||||
pub indent_style: Option<IndentStyle>,
|
||||
pub quote_style: Option<QuoteStyle>,
|
||||
|
@ -1044,6 +1046,9 @@ impl FormatConfiguration {
|
|||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn from_options(options: FormatOptions, project_root: &Path) -> Result<Self> {
|
||||
Ok(Self {
|
||||
// `--extension` is a hidden command-line argument that isn't supported in configuration
|
||||
// files at present.
|
||||
extension: None,
|
||||
exclude: options.exclude.map(|paths| {
|
||||
paths
|
||||
.into_iter()
|
||||
|
@ -1077,18 +1082,19 @@ impl FormatConfiguration {
|
|||
|
||||
#[must_use]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn combine(self, other: Self) -> Self {
|
||||
pub fn combine(self, config: Self) -> Self {
|
||||
Self {
|
||||
exclude: self.exclude.or(other.exclude),
|
||||
preview: self.preview.or(other.preview),
|
||||
indent_style: self.indent_style.or(other.indent_style),
|
||||
quote_style: self.quote_style.or(other.quote_style),
|
||||
magic_trailing_comma: self.magic_trailing_comma.or(other.magic_trailing_comma),
|
||||
line_ending: self.line_ending.or(other.line_ending),
|
||||
docstring_code_format: self.docstring_code_format.or(other.docstring_code_format),
|
||||
exclude: self.exclude.or(config.exclude),
|
||||
preview: self.preview.or(config.preview),
|
||||
extension: self.extension.or(config.extension),
|
||||
indent_style: self.indent_style.or(config.indent_style),
|
||||
quote_style: self.quote_style.or(config.quote_style),
|
||||
magic_trailing_comma: self.magic_trailing_comma.or(config.magic_trailing_comma),
|
||||
line_ending: self.line_ending.or(config.line_ending),
|
||||
docstring_code_format: self.docstring_code_format.or(config.docstring_code_format),
|
||||
docstring_code_line_width: self
|
||||
.docstring_code_line_width
|
||||
.or(other.docstring_code_line_width),
|
||||
.or(config.docstring_code_line_width),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use path_absolutize::path_dedot;
|
||||
use ruff_cache::cache_dir;
|
||||
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
||||
use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes};
|
||||
use ruff_linter::settings::types::{
|
||||
ExtensionMapping, FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::settings::LinterSettings;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_python_ast::PySourceType;
|
||||
|
@ -116,6 +118,7 @@ impl FileResolverSettings {
|
|||
#[derive(CacheKey, Clone, Debug)]
|
||||
pub struct FormatterSettings {
|
||||
pub exclude: FilePatternSet,
|
||||
pub extension: ExtensionMapping,
|
||||
pub preview: PreviewMode,
|
||||
pub target_version: ruff_python_formatter::PythonVersion,
|
||||
|
||||
|
@ -177,6 +180,7 @@ impl Default for FormatterSettings {
|
|||
|
||||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
extension: ExtensionMapping::default(),
|
||||
target_version: default_options.target_version(),
|
||||
preview: PreviewMode::Disabled,
|
||||
line_width: default_options.line_width(),
|
||||
|
|
|
@ -528,6 +528,8 @@ Options:
|
|||
Enable preview mode; checks will include unstable rules and fixes. Use `--no-preview` to disable
|
||||
--config <CONFIG>
|
||||
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
|
||||
--extension <EXTENSION>
|
||||
List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`
|
||||
--statistics
|
||||
Show counts for every rule with at least one violation
|
||||
--add-noqa
|
||||
|
@ -604,6 +606,8 @@ Options:
|
|||
Avoid writing any formatted files back; instead, exit with a non-zero status code and the difference between the current file and how the formatted file would look like
|
||||
--config <CONFIG>
|
||||
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
|
||||
--extension <EXTENSION>
|
||||
List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312]
|
||||
--preview
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue